taskflow
This commit is contained in:
+282
-370
@@ -8,7 +8,7 @@ use pdfium_render::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
use tokio::task;
|
||||
use tracing::{error, info, warn};
|
||||
use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -27,16 +27,20 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
analyze::determine_thumbnail_support,
|
||||
common::{load_document_version, load_version_assets},
|
||||
fetch_version_object, handle_fetch_error, JobExecution, JobHandler,
|
||||
job_execution_from_task_error,
|
||||
taskflow::{
|
||||
document::DocumentVersionTaskContext, BoxedTask, Task, TaskContext, TaskError,
|
||||
TaskExecutor, TaskPlanner, TaskResult,
|
||||
},
|
||||
JobExecution, JobHandler,
|
||||
};
|
||||
|
||||
const THUMBNAIL_WIDTH: u32 = 512;
|
||||
const THUMBNAIL_HEIGHT: u32 = 512;
|
||||
pub const THUMBNAIL_WIDTH: u32 = 512;
|
||||
pub const THUMBNAIL_HEIGHT: u32 = 512;
|
||||
const PREVIEW_WIDTH: u32 = THUMBNAIL_WIDTH * 4;
|
||||
const PREVIEW_HEIGHT: u32 = THUMBNAIL_HEIGHT * 4;
|
||||
const THUMBNAIL_ASSET_TYPE: &str = "thumbnail";
|
||||
const PREVIEW_ASSET_TYPE: &str = "preview";
|
||||
pub const THUMBNAIL_ASSET_TYPE: &str = "thumbnail";
|
||||
pub const PREVIEW_ASSET_TYPE: &str = "preview";
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct ThumbnailPayload {
|
||||
@@ -75,268 +79,109 @@ impl JobHandler for GenerateThumbnailsJob {
|
||||
}
|
||||
};
|
||||
|
||||
let state_clone = state.clone();
|
||||
let tenant_id = job.tenant_id;
|
||||
let payload_for_context = payload.clone();
|
||||
let initial = match task::spawn_blocking(move || {
|
||||
load_thumbnail_context(state_clone, tenant_id, payload_for_context)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(ctx)) => ctx,
|
||||
Ok(Err(err)) => {
|
||||
warn!(job_id = %job.id, error = %err, "thumbnail job will retry");
|
||||
return JobExecution::Retry {
|
||||
delay: Duration::from_secs(30),
|
||||
error: err,
|
||||
};
|
||||
}
|
||||
Err(join_err) => {
|
||||
error!(job_id = %job.id, error = %join_err, "thumbnail task panicked");
|
||||
return JobExecution::Retry {
|
||||
delay: Duration::from_secs(60),
|
||||
error: format!("worker panicked: {join_err}"),
|
||||
};
|
||||
}
|
||||
};
|
||||
let mut context = DocumentVersionTaskContext::new(
|
||||
job.id,
|
||||
JOB_GENERATE_THUMBNAILS,
|
||||
job.tenant_id,
|
||||
payload.document_id,
|
||||
payload.document_version_id,
|
||||
payload.force,
|
||||
state.config.worker_max_document_bytes,
|
||||
state.clone(),
|
||||
storage,
|
||||
);
|
||||
|
||||
if initial.skip {
|
||||
info!(job_id = %job.id, "thumbnails already exist; skipping");
|
||||
return JobExecution::Success;
|
||||
let planner = ThumbnailPlanner {
|
||||
force: payload.force,
|
||||
};
|
||||
match TaskExecutor::run(&planner, &mut context).await {
|
||||
Ok(()) => JobExecution::Success,
|
||||
Err(err) => job_execution_from_task_error(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThumbnailPlanner {
|
||||
force: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TaskPlanner<DocumentVersionTaskContext> for ThumbnailPlanner {
|
||||
async fn plan(
|
||||
&self,
|
||||
_ctx: &mut DocumentVersionTaskContext,
|
||||
) -> TaskResult<Vec<BoxedTask<DocumentVersionTaskContext>>> {
|
||||
Ok(vec![Box::new(GenerateThumbnailsTask::new(self.force))])
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GenerateThumbnailsTask {
|
||||
force: bool,
|
||||
}
|
||||
|
||||
impl GenerateThumbnailsTask {
|
||||
pub fn new(force: bool) -> Self {
|
||||
Self { force }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Task<DocumentVersionTaskContext> for GenerateThumbnailsTask {
|
||||
fn name(&self) -> &'static str {
|
||||
"generate-thumbnails"
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &mut DocumentVersionTaskContext) -> TaskResult<()> {
|
||||
let context = build_thumbnail_context(ctx, self.force).await?;
|
||||
|
||||
if context.skip {
|
||||
info!(job_id = %ctx.job_id(), "thumbnails already exist; skipping");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bytes = match fetch_version_object(
|
||||
&initial.version,
|
||||
&storage,
|
||||
&initial.version.s3_key,
|
||||
state.config.worker_max_document_bytes,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => return handle_fetch_error(&job, err, "thumbnail fetch failed; will retry"),
|
||||
};
|
||||
|
||||
let generation = match generate_preview_and_thumbnail(&initial.document, &bytes) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return JobExecution::Failed { error: err };
|
||||
}
|
||||
};
|
||||
let bytes = ctx.buffered_object().await?;
|
||||
let generation =
|
||||
generate_preview_and_thumbnail(&context.document, bytes).map_err(TaskError::fail)?;
|
||||
|
||||
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;
|
||||
let tenant_id = initial.tenant_id;
|
||||
match task::spawn_blocking(move || {
|
||||
persist_document_page_count(
|
||||
state_clone,
|
||||
tenant_id,
|
||||
document_id,
|
||||
version_id,
|
||||
page_count,
|
||||
let state = ctx.state().clone();
|
||||
let tenant_id = context.tenant_id;
|
||||
let document_id = context.document.id;
|
||||
let version_id = context.version.id;
|
||||
task::spawn_blocking(move || {
|
||||
persist_document_page_count(state, tenant_id, document_id, version_id, page_count)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
TaskError::retry(
|
||||
Duration::from_secs(60),
|
||||
format!("page count task panicked: {err}"),
|
||||
)
|
||||
})
|
||||
.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}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
})?
|
||||
.map_err(|err| TaskError::retry(Duration::from_secs(30), err))?;
|
||||
}
|
||||
|
||||
if let Some(existing_preview) = &initial.existing_preview {
|
||||
for object in &initial.existing_preview_objects {
|
||||
if let Err(err) = storage.delete_object(&object.s3_key).await {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
error = %err,
|
||||
s3_key = %object.s3_key,
|
||||
"failed to delete existing preview object"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let tenant_id = initial.document.tenant_id;
|
||||
let asset_id = existing_preview.id;
|
||||
let state_clone = state.clone();
|
||||
match task::spawn_blocking(move || -> AppResult<()> {
|
||||
let mut conn = state_clone.db_for_tenant(tenant_id)?;
|
||||
delete_asset(&mut conn, tenant_id, asset_id)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
error = ?err,
|
||||
asset_id = %asset_id,
|
||||
"failed to remove preview metadata after deletion"
|
||||
);
|
||||
}
|
||||
Err(join_err) => {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
error = %join_err,
|
||||
asset_id = %asset_id,
|
||||
"failed to remove preview metadata: task panicked"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(existing_thumbnail) = &initial.existing_thumbnail {
|
||||
for object in &initial.existing_thumbnail_objects {
|
||||
if let Err(err) = storage.delete_object(&object.s3_key).await {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
error = %err,
|
||||
s3_key = %object.s3_key,
|
||||
"failed to delete existing thumbnail object"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let tenant_id = initial.document.tenant_id;
|
||||
let asset_id = existing_thumbnail.id;
|
||||
let state_clone = state.clone();
|
||||
match task::spawn_blocking(move || -> AppResult<()> {
|
||||
let mut conn = state_clone.db_for_tenant(tenant_id)?;
|
||||
delete_asset(&mut conn, tenant_id, asset_id)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
error = ?err,
|
||||
asset_id = %asset_id,
|
||||
"failed to remove thumbnail metadata after deletion"
|
||||
);
|
||||
}
|
||||
Err(join_err) => {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
error = %join_err,
|
||||
asset_id = %asset_id,
|
||||
"failed to remove thumbnail metadata: task panicked"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
remove_existing_assets(ctx, &context).await;
|
||||
|
||||
let preview_asset_id = Uuid::new_v4();
|
||||
|
||||
let thumbnail_asset_id = Uuid::new_v4();
|
||||
|
||||
let mut preview_objects: Vec<AssetObjectPersistence> =
|
||||
Vec::with_capacity(generation.preview.objects.len());
|
||||
for (index, image) in generation.preview.objects.iter().enumerate() {
|
||||
if index + 1 > i32::MAX as usize {
|
||||
return JobExecution::Failed {
|
||||
error: "too many preview objects".to_string(),
|
||||
};
|
||||
}
|
||||
let ordinal = (index + 1) as i32;
|
||||
let s3_key = document_asset_object_key(
|
||||
initial.document.id,
|
||||
initial.version.version_number,
|
||||
PREVIEW_ASSET_TYPE,
|
||||
preview_asset_id,
|
||||
ordinal,
|
||||
);
|
||||
|
||||
if let Err(err) = storage
|
||||
.put_object(
|
||||
&s3_key,
|
||||
image.image_bytes.clone(),
|
||||
Some("image/webp".into()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(job_id = %job.id, error = %err, ordinal, "failed to upload preview; retrying");
|
||||
return JobExecution::Retry {
|
||||
delay: Duration::from_secs(30),
|
||||
error: err.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
preview_objects.push(AssetObjectPersistence {
|
||||
ordinal,
|
||||
s3_key,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
|
||||
let mut thumbnail_objects: Vec<AssetObjectPersistence> =
|
||||
Vec::with_capacity(generation.thumbnail.objects.len());
|
||||
for (index, image) in generation.thumbnail.objects.iter().enumerate() {
|
||||
if index + 1 > i32::MAX as usize {
|
||||
return JobExecution::Failed {
|
||||
error: "too many thumbnail objects".to_string(),
|
||||
};
|
||||
}
|
||||
let ordinal = (index + 1) as i32;
|
||||
let s3_key = document_asset_object_key(
|
||||
initial.document.id,
|
||||
initial.version.version_number,
|
||||
THUMBNAIL_ASSET_TYPE,
|
||||
thumbnail_asset_id,
|
||||
ordinal,
|
||||
);
|
||||
|
||||
if let Err(err) = storage
|
||||
.put_object(
|
||||
&s3_key,
|
||||
image.image_bytes.clone(),
|
||||
Some("image/webp".into()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(job_id = %job.id, error = %err, ordinal, "failed to upload thumbnail; retrying");
|
||||
return JobExecution::Retry {
|
||||
delay: Duration::from_secs(30),
|
||||
error: err.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
thumbnail_objects.push(AssetObjectPersistence {
|
||||
ordinal,
|
||||
s3_key,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
let preview_objects = upload_generated_objects(
|
||||
ctx,
|
||||
&context,
|
||||
PREVIEW_ASSET_TYPE,
|
||||
preview_asset_id,
|
||||
&generation.preview,
|
||||
)
|
||||
.await?;
|
||||
let thumbnail_objects = upload_generated_objects(
|
||||
ctx,
|
||||
&context,
|
||||
THUMBNAIL_ASSET_TYPE,
|
||||
thumbnail_asset_id,
|
||||
&generation.thumbnail,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let asset_persistences = vec![
|
||||
AssetPersistence {
|
||||
@@ -351,33 +196,186 @@ impl JobHandler for GenerateThumbnailsJob {
|
||||
},
|
||||
];
|
||||
|
||||
let state_clone = state.clone();
|
||||
match task::spawn_blocking(move || {
|
||||
persist_assets_metadata(state_clone, &initial, &asset_persistences)
|
||||
let state = ctx.state().clone();
|
||||
let tenant_id = context.document.tenant_id;
|
||||
let version_id = context.version.id;
|
||||
task::spawn_blocking(move || {
|
||||
persist_assets_metadata(state, tenant_id, version_id, &asset_persistences)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => {
|
||||
warn!(job_id = %job.id, error = %err, "failed to persist thumbnail metadata; retrying");
|
||||
return JobExecution::Retry {
|
||||
delay: Duration::from_secs(30),
|
||||
error: err,
|
||||
};
|
||||
}
|
||||
Err(join_err) => {
|
||||
error!(job_id = %job.id, error = %join_err, "thumbnail metadata update panicked");
|
||||
return JobExecution::Retry {
|
||||
delay: Duration::from_secs(30),
|
||||
error: format!("metadata update panic: {join_err}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
.map_err(|err| {
|
||||
TaskError::retry(
|
||||
Duration::from_secs(60),
|
||||
format!("thumbnail metadata task panicked: {err}"),
|
||||
)
|
||||
})?
|
||||
.map_err(|err| TaskError::retry(Duration::from_secs(30), err))?;
|
||||
|
||||
JobExecution::Success
|
||||
ctx.invalidate_asset_cache();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_thumbnail_context(
|
||||
ctx: &mut DocumentVersionTaskContext,
|
||||
force: bool,
|
||||
) -> TaskResult<ThumbnailContext> {
|
||||
let document = ctx.document().await?.clone();
|
||||
let version = ctx.version().await?.clone();
|
||||
let tenant_id = ctx.tenant_id();
|
||||
|
||||
let (supported, _) = determine_thumbnail_support(&document);
|
||||
if !supported {
|
||||
return Ok(ThumbnailContext {
|
||||
document,
|
||||
version,
|
||||
existing_thumbnail: None,
|
||||
existing_thumbnail_objects: Vec::new(),
|
||||
existing_preview: None,
|
||||
existing_preview_objects: Vec::new(),
|
||||
skip: true,
|
||||
tenant_id,
|
||||
});
|
||||
}
|
||||
|
||||
let assets = ctx.assets().await?;
|
||||
let (existing_thumbnail, existing_thumbnail_objects) = assets
|
||||
.get(THUMBNAIL_ASSET_TYPE)
|
||||
.map(|entry| (Some(entry.asset.clone()), entry.objects.clone()))
|
||||
.unwrap_or((None, Vec::new()));
|
||||
let (existing_preview, existing_preview_objects) = assets
|
||||
.get(PREVIEW_ASSET_TYPE)
|
||||
.map(|entry| (Some(entry.asset.clone()), entry.objects.clone()))
|
||||
.unwrap_or((None, Vec::new()));
|
||||
|
||||
let expected_cardinality = expected_asset_cardinality(&document, &version);
|
||||
let preview_cardinality = existing_preview
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.cardinality)
|
||||
.unwrap_or_else(|| existing_preview_objects.len() as i32);
|
||||
let thumbnail_cardinality = existing_thumbnail
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.cardinality)
|
||||
.unwrap_or_else(|| existing_thumbnail_objects.len() as i32);
|
||||
let needs_regeneration = preview_cardinality < expected_cardinality
|
||||
|| thumbnail_cardinality < expected_cardinality
|
||||
|| (existing_preview_objects.len() as i32) < expected_cardinality
|
||||
|| (existing_thumbnail_objects.len() as i32) < expected_cardinality;
|
||||
|
||||
let skip =
|
||||
existing_thumbnail.is_some() && existing_preview.is_some() && !force && !needs_regeneration;
|
||||
|
||||
Ok(ThumbnailContext {
|
||||
document,
|
||||
version,
|
||||
existing_thumbnail,
|
||||
existing_thumbnail_objects,
|
||||
existing_preview,
|
||||
existing_preview_objects,
|
||||
skip,
|
||||
tenant_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn remove_existing_assets(ctx: &DocumentVersionTaskContext, context: &ThumbnailContext) {
|
||||
if let Some(existing_preview) = &context.existing_preview {
|
||||
delete_asset_with_objects(ctx, existing_preview, &context.existing_preview_objects).await;
|
||||
}
|
||||
|
||||
if let Some(existing_thumbnail) = &context.existing_thumbnail {
|
||||
delete_asset_with_objects(ctx, existing_thumbnail, &context.existing_thumbnail_objects)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_asset_with_objects(
|
||||
ctx: &DocumentVersionTaskContext,
|
||||
asset: &DocumentAsset,
|
||||
objects: &[DocumentAssetObject],
|
||||
) {
|
||||
for object in objects {
|
||||
if let Err(err) = ctx.storage().delete_object(&object.s3_key).await {
|
||||
warn!(
|
||||
job_id = %ctx.job_id(),
|
||||
error = %err,
|
||||
s3_key = %object.s3_key,
|
||||
"failed to delete existing asset object"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let tenant_id = ctx.tenant_id();
|
||||
let asset_id = asset.id;
|
||||
let state = ctx.state().clone();
|
||||
match task::spawn_blocking(move || -> AppResult<()> {
|
||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||
delete_asset(&mut conn, tenant_id, asset_id)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => {
|
||||
warn!(
|
||||
job_id = %ctx.job_id(),
|
||||
asset_id = %asset_id,
|
||||
error = ?err,
|
||||
"failed to delete asset metadata"
|
||||
);
|
||||
}
|
||||
Err(join_err) => {
|
||||
warn!(
|
||||
job_id = %ctx.job_id(),
|
||||
asset_id = %asset_id,
|
||||
error = %join_err,
|
||||
"failed to delete asset metadata task panicked"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn upload_generated_objects(
|
||||
ctx: &DocumentVersionTaskContext,
|
||||
context: &ThumbnailContext,
|
||||
asset_type: &str,
|
||||
asset_id: Uuid,
|
||||
asset: &GeneratedAsset,
|
||||
) -> TaskResult<Vec<AssetObjectPersistence>> {
|
||||
let mut objects = Vec::with_capacity(asset.objects.len());
|
||||
|
||||
for (index, image) in asset.objects.iter().enumerate() {
|
||||
if index + 1 > i32::MAX as usize {
|
||||
return Err(TaskError::fail("too many generated asset objects"));
|
||||
}
|
||||
let ordinal = (index + 1) as i32;
|
||||
let s3_key = document_asset_object_key(
|
||||
context.document.id,
|
||||
context.version.version_number,
|
||||
asset_type,
|
||||
asset_id,
|
||||
ordinal,
|
||||
);
|
||||
|
||||
ctx.storage()
|
||||
.put_object(
|
||||
&s3_key,
|
||||
image.image_bytes.clone(),
|
||||
Some("image/webp".into()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| TaskError::retry(Duration::from_secs(30), err.to_string()))?;
|
||||
|
||||
objects.push(AssetObjectPersistence {
|
||||
ordinal,
|
||||
s3_key,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
struct ThumbnailContext {
|
||||
document: Document,
|
||||
version: DocumentVersion,
|
||||
@@ -418,83 +416,11 @@ struct AssetPersistence {
|
||||
objects: Vec<AssetObjectPersistence>,
|
||||
}
|
||||
|
||||
fn load_thumbnail_context(
|
||||
state: Arc<AppState>,
|
||||
tenant_id: Uuid,
|
||||
payload: ThumbnailPayload,
|
||||
) -> Result<ThumbnailContext, String> {
|
||||
let base = load_document_version(
|
||||
state.as_ref(),
|
||||
tenant_id,
|
||||
payload.document_id,
|
||||
payload.document_version_id,
|
||||
)?;
|
||||
|
||||
let mut conn = state
|
||||
.db_for_tenant(base.tenant_id)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
let mut assets = load_version_assets(
|
||||
&mut conn,
|
||||
base.tenant_id,
|
||||
base.version.id,
|
||||
&[THUMBNAIL_ASSET_TYPE, PREVIEW_ASSET_TYPE],
|
||||
)?;
|
||||
|
||||
let (existing_thumbnail, existing_thumbnail_objects) = assets
|
||||
.remove(THUMBNAIL_ASSET_TYPE)
|
||||
.map(|entry| (Some(entry.asset), entry.objects))
|
||||
.unwrap_or((None, Vec::new()));
|
||||
|
||||
let (existing_preview, existing_preview_objects) = assets
|
||||
.remove(PREVIEW_ASSET_TYPE)
|
||||
.map(|entry| (Some(entry.asset), entry.objects))
|
||||
.unwrap_or((None, Vec::new()));
|
||||
|
||||
let (supported, _) = determine_thumbnail_support(&base.document);
|
||||
if !supported {
|
||||
return Err("thumbnail generation not supported for this document".into());
|
||||
}
|
||||
|
||||
let expected_cardinality = expected_asset_cardinality(&base.document, &base.version);
|
||||
let preview_cardinality = existing_preview
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.cardinality)
|
||||
.unwrap_or_else(|| existing_preview_objects.len() as i32);
|
||||
let thumbnail_cardinality = existing_thumbnail
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.cardinality)
|
||||
.unwrap_or_else(|| existing_thumbnail_objects.len() as i32);
|
||||
|
||||
let needs_regeneration = preview_cardinality < expected_cardinality
|
||||
|| thumbnail_cardinality < expected_cardinality
|
||||
|| (existing_preview_objects.len() as i32) < expected_cardinality
|
||||
|| (existing_thumbnail_objects.len() as i32) < expected_cardinality;
|
||||
|
||||
let skip = existing_thumbnail.is_some()
|
||||
&& existing_preview.is_some()
|
||||
&& !payload.force
|
||||
&& !needs_regeneration;
|
||||
|
||||
Ok(ThumbnailContext {
|
||||
document: base.document,
|
||||
version: base.version,
|
||||
existing_thumbnail,
|
||||
existing_thumbnail_objects,
|
||||
existing_preview,
|
||||
existing_preview_objects,
|
||||
skip,
|
||||
tenant_id: base.tenant_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_preview_and_thumbnail(
|
||||
document: &Document,
|
||||
bytes: &[u8],
|
||||
) -> Result<GeneratedAssets, String> {
|
||||
let is_pdf = document_is_pdf(document);
|
||||
|
||||
if is_pdf {
|
||||
if document_is_pdf(document) {
|
||||
let pdf_assets = generate_pdf_assets(bytes)?;
|
||||
Ok(GeneratedAssets {
|
||||
preview: pdf_assets.preview,
|
||||
@@ -623,28 +549,14 @@ fn encode_dynamic_image(image: image::DynamicImage) -> Result<GeneratedImage, St
|
||||
|
||||
fn persist_assets_metadata(
|
||||
state: Arc<AppState>,
|
||||
context: &ThumbnailContext,
|
||||
tenant_id: Uuid,
|
||||
version_id: Uuid,
|
||||
assets: &[AssetPersistence],
|
||||
) -> Result<(), String> {
|
||||
let tenant_id = context.document.tenant_id;
|
||||
let mut conn = state
|
||||
.db_for_tenant(tenant_id)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
if let Some(existing_preview) = &context.existing_preview {
|
||||
diesel::delete(document_assets::table.filter(document_assets::id.eq(existing_preview.id)))
|
||||
.execute(&mut conn)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
}
|
||||
|
||||
if let Some(existing_thumbnail) = &context.existing_thumbnail {
|
||||
diesel::delete(
|
||||
document_assets::table.filter(document_assets::id.eq(existing_thumbnail.id)),
|
||||
)
|
||||
.execute(&mut conn)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
}
|
||||
|
||||
for asset in assets {
|
||||
if asset.objects.is_empty() {
|
||||
return Err(format!(
|
||||
@@ -661,7 +573,7 @@ fn persist_assets_metadata(
|
||||
|
||||
let new_asset = NewDocumentAsset {
|
||||
id: asset.asset_id,
|
||||
document_version_id: context.version.id,
|
||||
document_version_id: version_id,
|
||||
asset_type: asset.asset_type.to_string(),
|
||||
mime_type: "image/webp".to_string(),
|
||||
metadata: json!({
|
||||
@@ -688,7 +600,7 @@ fn persist_assets_metadata(
|
||||
|
||||
diesel::delete(
|
||||
document_asset_objects::table
|
||||
.filter(document_asset_objects::asset_id.eq(asset.asset_id))
|
||||
.filter(document_asset_objects::asset_id.eq(new_asset.id))
|
||||
.filter(document_asset_objects::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.execute(&mut conn)
|
||||
@@ -707,7 +619,7 @@ fn persist_assets_metadata(
|
||||
|
||||
let new_object = NewDocumentAssetObject {
|
||||
id: Uuid::new_v4(),
|
||||
asset_id: asset.asset_id,
|
||||
asset_id: new_asset.id,
|
||||
ordinal: object.ordinal,
|
||||
s3_key: object.s3_key.clone(),
|
||||
metadata: object_metadata,
|
||||
|
||||
Reference in New Issue
Block a user