From f2c056a1515be493b195f3f5db5886c2ddf36700 Mon Sep 17 00:00:00 2001 From: Angelo Kontaxis Date: Thu, 2 Apr 2026 03:48:22 +0100 Subject: [PATCH] fix: add migration to update existing files to be animated (#705) * fix: add migration to update existing files to be animated Signed-off-by: Zomatree * Revert "fix: add migration to update existing files to be animated" This reverts commit 4e1f1c116cd2a7f648889ce8f0c8a951024fb90e. Signed-off-by: Zomatree * fix: calculate animated for existing files when fetched Signed-off-by: Zomatree --------- Signed-off-by: Zomatree --- .../database/src/models/file_hashes/model.rs | 3 +- .../database/src/models/file_hashes/ops.rs | 6 +++ .../src/models/file_hashes/ops/mongodb.rs | 23 ++++++++++ .../src/models/file_hashes/ops/reference.rs | 26 +++++++++++- crates/core/models/src/v0/files.rs | 2 +- crates/services/autumn/src/api.rs | 42 ++++++++++++++++--- crates/services/autumn/src/metadata.rs | 2 +- 7 files changed, 92 insertions(+), 12 deletions(-) diff --git a/crates/core/database/src/models/file_hashes/model.rs b/crates/core/database/src/models/file_hashes/model.rs index b25abb12..dfc9b96e 100644 --- a/crates/core/database/src/models/file_hashes/model.rs +++ b/crates/core/database/src/models/file_hashes/model.rs @@ -46,8 +46,7 @@ auto_derived!( width: isize, height: isize, thumbhash: Option>, - #[serde(default)] - animated: bool, + animated: Option, }, /// File is a video with specific dimensions Video { width: isize, height: isize }, diff --git a/crates/core/database/src/models/file_hashes/ops.rs b/crates/core/database/src/models/file_hashes/ops.rs index 4b249e4c..cdd6c936 100644 --- a/crates/core/database/src/models/file_hashes/ops.rs +++ b/crates/core/database/src/models/file_hashes/ops.rs @@ -17,6 +17,12 @@ pub trait AbstractAttachmentHashes: Sync + Send { /// Update an attachment hash nonce value. async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()>; + /// Updates the attachments animated metadata value. + /// + /// The primary use for this is to update the metadata for existing uploaded files, this + /// can only be used for images. + async fn set_attachment_hash_animated(&self, hash: &str, animated: bool) -> Result<()>; + /// Delete attachment hash by id. async fn delete_attachment_hash(&self, id: &str) -> Result<()>; } diff --git a/crates/core/database/src/models/file_hashes/ops/mongodb.rs b/crates/core/database/src/models/file_hashes/ops/mongodb.rs index e2f06067..9e67a0a5 100644 --- a/crates/core/database/src/models/file_hashes/ops/mongodb.rs +++ b/crates/core/database/src/models/file_hashes/ops/mongodb.rs @@ -48,6 +48,29 @@ impl AbstractAttachmentHashes for MongoDb { .map_err(|_| create_database_error!("update_one", COL)) } + /// Updates the attachments animated metadata value. + /// + /// The primary use for this is to update the metadata for existing uploaded files, this + /// can only be used for images. + async fn set_attachment_hash_animated(&self, hash: &str, animated: bool) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": hash, + "metadata.type": "Image", + "metadata.animated": { "$exists": false }, + }, + doc! { + "$set": { + "metadata.animated": animated + } + }, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", COL)) + } + /// Delete attachment hash by id. async fn delete_attachment_hash(&self, id: &str) -> Result<()> { query!(self, delete_one_by_id, COL, id).map(|_| ()) diff --git a/crates/core/database/src/models/file_hashes/ops/reference.rs b/crates/core/database/src/models/file_hashes/ops/reference.rs index 7733523e..3c54d9e2 100644 --- a/crates/core/database/src/models/file_hashes/ops/reference.rs +++ b/crates/core/database/src/models/file_hashes/ops/reference.rs @@ -1,7 +1,6 @@ use revolt_result::Result; -use crate::FileHash; -use crate::ReferenceDb; +use crate::{FileHash, Metadata, ReferenceDb}; use super::AbstractAttachmentHashes; @@ -39,6 +38,29 @@ impl AbstractAttachmentHashes for ReferenceDb { } } + /// Updates the attachments animated metadata value. + /// + /// The primary use for this is to update the metadata for existing uploaded files, this + /// can only be used for images. + async fn set_attachment_hash_animated(&self, hash: &str, animated: bool) -> Result<()> { + let mut hashes = self.file_hashes.lock().await; + if let Some(FileHash { + metadata: + Metadata::Image { + animated: Some(animated_metadata), + .. + }, + .. + }) = hashes.get_mut(hash) + { + *animated_metadata = animated; + + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + /// Delete attachment hash by id. async fn delete_attachment_hash(&self, id: &str) -> Result<()> { let mut file_hashes = self.file_hashes.lock().await; diff --git a/crates/core/models/src/v0/files.rs b/crates/core/models/src/v0/files.rs index 32ccf0e1..2d68b7f2 100644 --- a/crates/core/models/src/v0/files.rs +++ b/crates/core/models/src/v0/files.rs @@ -51,7 +51,7 @@ auto_derived!( width: usize, height: usize, thumbhash: Option>, - animated: bool, + animated: Option, }, /// File is a video with specific dimensions Video { width: usize, height: usize }, diff --git a/crates/services/autumn/src/api.rs b/crates/services/autumn/src/api.rs index 4018e72c..49a61c43 100644 --- a/crates/services/autumn/src/api.rs +++ b/crates/services/autumn/src/api.rs @@ -1,5 +1,5 @@ use std::{ - io::{Cursor, Read}, + io::{Cursor, Read, Write}, time::Duration, }; @@ -15,9 +15,10 @@ use lazy_static::lazy_static; use revolt_config::{config, report_internal_error}; use revolt_database::{iso8601_timestamp::Timestamp, Database, FileHash, Metadata, User}; use revolt_files::{ - create_thumbnail, decode_image, fetch_from_s3, upload_to_s3, AUTHENTICATION_TAG_SIZE_BYTES, + create_thumbnail, decode_image, fetch_from_s3, is_animated, upload_to_s3, + AUTHENTICATION_TAG_SIZE_BYTES, }; -use revolt_result::{create_error, Error, Result}; +use revolt_result::{create_error, Error, Result, ToRevoltError}; use serde::{Deserialize, Serialize}; use sha2::Digest; use tempfile::NamedTempFile; @@ -25,7 +26,9 @@ use tokio::time::Instant; use tower_http::cors::{AllowHeaders, Any, CorsLayer}; use utoipa::ToSchema; -use crate::{exif::strip_metadata, metadata::generate_metadata, mime_type::determine_mime_type, AppState}; +use crate::{ + exif::strip_metadata, metadata::generate_metadata, mime_type::determine_mime_type, AppState, +}; /// Build the API router pub async fn router() -> Router { @@ -380,7 +383,30 @@ async fn fetch_preview( let hash = file.as_hash(&db).await?; - let is_animated = matches!(hash.metadata, Metadata::Image { animated: true, .. }); + let mut data = None; + + // If animated is unset, check the file contents to see if it is animated and update the filehash + let is_animated = match &hash.metadata { + Metadata::Image { + animated: Some(value), + .. + } => *value, + Metadata::Image { animated: None, .. } => { + let file_data = retrieve_file_by_hash(&hash).await?; + + let mut named_file = NamedTempFile::new().to_internal_error()?; + named_file.write(&file_data).to_internal_error()?; + + data = Some(file_data); + + // If it fails for some reason, set it to not be animated + let animated = is_animated(&named_file, &hash.content_type).unwrap_or(false); + db.set_attachment_hash_animated(&hash.id, animated).await?; + + animated + } + _ => false, + }; // Only process image files and don't process GIFs if not avatar or icon if !matches!(hash.metadata, Metadata::Image { .. }) @@ -392,7 +418,11 @@ async fn fetch_preview( } // Original image data - let data = retrieve_file_by_hash(&hash).await?; + let data = if let Some(data) = data { + data + } else { + retrieve_file_by_hash(&hash).await? + }; // Read image and create thumbnail let data = create_thumbnail( diff --git a/crates/services/autumn/src/metadata.rs b/crates/services/autumn/src/metadata.rs index 3493cf49..8e0c89cd 100644 --- a/crates/services/autumn/src/metadata.rs +++ b/crates/services/autumn/src/metadata.rs @@ -37,7 +37,7 @@ pub fn generate_metadata(f: &NamedTempFile, mime_type: &str) -> Metadata { thumbhash::rgba_to_thumb_hash(width as usize, height as usize, &rgba) }) .ok(), - animated: is_animated(f, mime_type).unwrap_or(false), + animated: is_animated(f, mime_type).or(Some(false)), }) .unwrap_or_default() } else if mime_type.starts_with("video/") {