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 <me@zomatree.live>

* Revert "fix: add migration to update existing files to be animated"

This reverts commit 4e1f1c116c.

Signed-off-by: Zomatree <me@zomatree.live>

* fix: calculate animated for existing files when fetched

Signed-off-by: Zomatree <me@zomatree.live>

---------

Signed-off-by: Zomatree <me@zomatree.live>
This commit is contained in:
Angelo Kontaxis
2026-04-02 03:48:22 +01:00
committed by GitHub
parent f30b729ca9
commit f2c056a151
7 changed files with 92 additions and 12 deletions

View File

@@ -46,8 +46,7 @@ auto_derived!(
width: isize,
height: isize,
thumbhash: Option<Vec<u8>>,
#[serde(default)]
animated: bool,
animated: Option<bool>,
},
/// File is a video with specific dimensions
Video { width: isize, height: isize },

View File

@@ -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<()>;
}

View File

@@ -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::<FileHash>(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(|_| ())

View File

@@ -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;

View File

@@ -51,7 +51,7 @@ auto_derived!(
width: usize,
height: usize,
thumbhash: Option<Vec<u8>>,
animated: bool,
animated: Option<bool>,
},
/// File is a video with specific dimensions
Video { width: usize, height: usize },

View File

@@ -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<AppState> {
@@ -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(

View File

@@ -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/") {