Save sizes of media attachments and other files to database

This commit is contained in:
silverpill 2023-01-19 21:57:29 +00:00
parent e3b51d0752
commit b958b8fb4c
14 changed files with 66 additions and 27 deletions

View file

@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added
- Save sizes of media attachments and other files to database.
### Security ### Security
- Validate emoji name before saving. - Validate emoji name before saving.

View file

@ -0,0 +1 @@
ALTER TABLE media_attachment ADD COLUMN file_size INTEGER;

View file

@ -106,8 +106,9 @@ CREATE TABLE post_reaction (
CREATE TABLE media_attachment ( CREATE TABLE media_attachment (
id UUID PRIMARY KEY, id UUID PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, owner_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
media_type VARCHAR(50),
file_name VARCHAR(200) NOT NULL, file_name VARCHAR(200) NOT NULL,
file_size INTEGER,
media_type VARCHAR(50),
ipfs_cid VARCHAR(200), ipfs_cid VARCHAR(200),
post_id UUID REFERENCES post (id) ON DELETE CASCADE, post_id UUID REFERENCES post (id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()

View file

@ -9,9 +9,9 @@ use crate::activitypub::{
}; };
use crate::config::Instance; use crate::config::Instance;
use crate::database::DatabaseClient; use crate::database::DatabaseClient;
use crate::models::profiles::{ use crate::models::{
queries::{create_profile, update_profile}, profiles::queries::{create_profile, update_profile},
types::{ profiles::types::{
DbActorProfile, DbActorProfile,
ProfileImage, ProfileImage,
ProfileCreateData, ProfileCreateData,
@ -36,9 +36,10 @@ async fn fetch_actor_images(
ACTOR_IMAGE_MAX_SIZE, ACTOR_IMAGE_MAX_SIZE,
media_dir, media_dir,
).await { ).await {
Ok((file_name, maybe_media_type)) => { Ok((file_name, file_size, maybe_media_type)) => {
let image = ProfileImage::new( let image = ProfileImage::new(
file_name, file_name,
file_size,
maybe_media_type, maybe_media_type,
); );
Some(image) Some(image)
@ -59,9 +60,10 @@ async fn fetch_actor_images(
ACTOR_IMAGE_MAX_SIZE, ACTOR_IMAGE_MAX_SIZE,
media_dir, media_dir,
).await { ).await {
Ok((file_name, maybe_media_type)) => { Ok((file_name, file_size, maybe_media_type)) => {
let image = ProfileImage::new( let image = ProfileImage::new(
file_name, file_name,
file_size,
maybe_media_type, maybe_media_type,
); );
Some(image) Some(image)

View file

@ -110,7 +110,7 @@ pub async fn fetch_file(
maybe_media_type: Option<&str>, maybe_media_type: Option<&str>,
file_max_size: usize, file_max_size: usize,
output_dir: &Path, output_dir: &Path,
) -> Result<(String, Option<String>), FetchError> { ) -> Result<(String, usize, Option<String>), FetchError> {
let client = build_client(instance)?; let client = build_client(instance)?;
let request_builder = let request_builder =
build_request(instance, client, Method::GET, url); build_request(instance, client, Method::GET, url);
@ -123,7 +123,8 @@ pub async fn fetch_file(
}; };
}; };
let file_data = response.bytes().await?; let file_data = response.bytes().await?;
if file_data.len() > file_max_size { let file_size = file_data.len();
if file_size > file_max_size {
return Err(FetchError::OtherError("file is too large")); return Err(FetchError::OtherError("file is too large"));
}; };
let maybe_media_type = maybe_media_type let maybe_media_type = maybe_media_type
@ -148,7 +149,7 @@ pub async fn fetch_file(
output_dir, output_dir,
maybe_media_type.as_deref(), maybe_media_type.as_deref(),
)?; )?;
Ok((file_name, maybe_media_type)) Ok((file_name, file_size, maybe_media_type))
} }
pub async fn perform_webfinger_query( pub async fn perform_webfinger_query(

View file

@ -215,7 +215,7 @@ pub async fn handle_note(
}; };
let attachment_url = attachment.url let attachment_url = attachment.url
.ok_or(ValidationError("attachment URL is missing"))?; .ok_or(ValidationError("attachment URL is missing"))?;
let (file_name, maybe_media_type) = fetch_file( let (file_name, file_size, maybe_media_type) = fetch_file(
instance, instance,
&attachment_url, &attachment_url,
attachment.media_type.as_deref(), attachment.media_type.as_deref(),
@ -227,18 +227,19 @@ pub async fn handle_note(
ValidationError("failed to fetch attachment") ValidationError("failed to fetch attachment")
})?; })?;
log::info!("downloaded attachment {}", attachment_url); log::info!("downloaded attachment {}", attachment_url);
downloaded.push((file_name, maybe_media_type)); downloaded.push((file_name, file_size, maybe_media_type));
// Stop downloading if limit is reached // Stop downloading if limit is reached
if downloaded.len() >= ATTACHMENTS_MAX_NUM { if downloaded.len() >= ATTACHMENTS_MAX_NUM {
log::warn!("too many attachments"); log::warn!("too many attachments");
break; break;
}; };
}; };
for (file_name, maybe_media_type) in downloaded { for (file_name, file_size, maybe_media_type) in downloaded {
let db_attachment = create_attachment( let db_attachment = create_attachment(
db_client, db_client,
&author.id, &author.id,
file_name, file_name,
file_size,
maybe_media_type, maybe_media_type,
).await?; ).await?;
attachments.push(db_attachment.id); attachments.push(db_attachment.id);
@ -411,7 +412,7 @@ pub async fn handle_note(
Err(DatabaseError::NotFound("emoji")) => None, Err(DatabaseError::NotFound("emoji")) => None,
Err(other_error) => return Err(other_error.into()), Err(other_error) => return Err(other_error.into()),
}; };
let (file_name, maybe_media_type) = match fetch_file( let (file_name, file_size, maybe_media_type) = match fetch_file(
instance, instance,
&tag.icon.url, &tag.icon.url,
tag.icon.media_type.as_deref(), tag.icon.media_type.as_deref(),
@ -437,7 +438,7 @@ pub async fn handle_note(
}, },
}; };
log::info!("downloaded emoji {}", tag.icon.url); log::info!("downloaded emoji {}", tag.icon.url);
let image = EmojiImage { file_name, media_type }; let image = EmojiImage { file_name, file_size, media_type };
let emoji = if let Some(emoji_id) = maybe_emoji_id { let emoji = if let Some(emoji_id) = maybe_emoji_id {
update_emoji( update_emoji(
db_client, db_client,

View file

@ -237,7 +237,7 @@ fn process_b64_image_field_value(
None None
} else { } else {
// Decode and save file // Decode and save file
let (file_name, media_type) = save_b64_file( let (file_name, file_size, media_type) = save_b64_file(
&b64_data, &b64_data,
form_media_type, form_media_type,
output_dir, output_dir,
@ -245,6 +245,7 @@ fn process_b64_image_field_value(
)?; )?;
let image = ProfileImage::new( let image = ProfileImage::new(
file_name, file_name,
file_size,
Some(media_type), Some(media_type),
); );
Some(image) Some(image)
@ -487,7 +488,7 @@ mod tests {
#[test] #[test]
fn test_create_account_from_profile() { fn test_create_account_from_profile() {
let profile = DbActorProfile { let profile = DbActorProfile {
avatar: Some(ProfileImage::new("test".to_string(), None)), avatar: Some(ProfileImage::new("test".to_string(), 1000, None)),
..Default::default() ..Default::default()
}; };
let account = Account::from_profile(profile, INSTANCE_URL); let account = Account::from_profile(profile, INSTANCE_URL);

View file

@ -19,7 +19,7 @@ async fn create_attachment_view(
) -> Result<HttpResponse, HttpError> { ) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?; let db_client = &**get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).await?; let current_user = get_current_user(db_client, auth.token()).await?;
let (file_name, media_type) = save_b64_file( let (file_name, file_size, media_type) = save_b64_file(
&attachment_data.file, &attachment_data.file,
attachment_data.media_type.clone(), attachment_data.media_type.clone(),
&config.media_dir(), &config.media_dir(),
@ -29,6 +29,7 @@ async fn create_attachment_view(
db_client, db_client,
&current_user.id, &current_user.id,
file_name, file_name,
file_size,
Some(media_type), Some(media_type),
).await?; ).await?;
let attachment = Attachment::from_db( let attachment = Attachment::from_db(

View file

@ -40,13 +40,14 @@ pub fn save_b64_file(
maybe_media_type: Option<String>, maybe_media_type: Option<String>,
output_dir: &Path, output_dir: &Path,
maybe_expected_prefix: Option<&str>, maybe_expected_prefix: Option<&str>,
) -> Result<(String, String), UploadError> { ) -> Result<(String, usize, String), UploadError> {
let data = base64::decode(b64data)?; let file_data = base64::decode(b64data)?;
if data.len() > UPLOAD_MAX_SIZE { let file_size = file_data.len();
if file_size > UPLOAD_MAX_SIZE {
return Err(UploadError::TooLarge); return Err(UploadError::TooLarge);
}; };
// Sniff media type if not provided // Sniff media type if not provided
let media_type = maybe_media_type.or(sniff_media_type(&data)) let media_type = maybe_media_type.or(sniff_media_type(&file_data))
.ok_or(UploadError::InvalidMediaType)?; .ok_or(UploadError::InvalidMediaType)?;
if !SUPPORTED_MEDIA_TYPES.contains(&media_type.as_str()) { if !SUPPORTED_MEDIA_TYPES.contains(&media_type.as_str()) {
return Err(UploadError::InvalidMediaType); return Err(UploadError::InvalidMediaType);
@ -57,9 +58,9 @@ pub fn save_b64_file(
}; };
}; };
let file_name = save_file( let file_name = save_file(
data, file_data,
output_dir, output_dir,
Some(&media_type), Some(&media_type),
)?; )?;
Ok((file_name, media_type)) Ok((file_name, file_size, media_type))
} }

View file

@ -14,16 +14,31 @@ pub async fn create_attachment(
db_client: &impl DatabaseClient, db_client: &impl DatabaseClient,
owner_id: &Uuid, owner_id: &Uuid,
file_name: String, file_name: String,
file_size: usize,
media_type: Option<String>, media_type: Option<String>,
) -> Result<DbMediaAttachment, DatabaseError> { ) -> Result<DbMediaAttachment, DatabaseError> {
let attachment_id = new_uuid(); let attachment_id = new_uuid();
let file_size: i32 = file_size.try_into()
.expect("value should be within bounds");
let inserted_row = db_client.query_one( let inserted_row = db_client.query_one(
" "
INSERT INTO media_attachment (id, owner_id, media_type, file_name) INSERT INTO media_attachment (
VALUES ($1, $2, $3, $4) id,
owner_id,
file_name,
file_size,
media_type
)
VALUES ($1, $2, $3, $4, $5)
RETURNING media_attachment RETURNING media_attachment
", ",
&[&attachment_id, &owner_id, &media_type, &file_name], &[
&attachment_id,
&owner_id,
&file_name,
&file_size,
&media_type,
],
).await?; ).await?;
let db_attachment: DbMediaAttachment = inserted_row.try_get("media_attachment")?; let db_attachment: DbMediaAttachment = inserted_row.try_get("media_attachment")?;
Ok(db_attachment) Ok(db_attachment)

View file

@ -7,8 +7,9 @@ use uuid::Uuid;
pub struct DbMediaAttachment { pub struct DbMediaAttachment {
pub id: Uuid, pub id: Uuid,
pub owner_id: Uuid, pub owner_id: Uuid,
pub media_type: Option<String>,
pub file_name: String, pub file_name: String,
pub file_size: Option<i32>,
pub media_type: Option<String>,
pub ipfs_cid: Option<String>, pub ipfs_cid: Option<String>,
pub post_id: Option<Uuid>, pub post_id: Option<Uuid>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,

View file

@ -129,6 +129,7 @@ mod tests {
let hostname = "example.org"; let hostname = "example.org";
let image = EmojiImage { let image = EmojiImage {
file_name: "test.png".to_string(), file_name: "test.png".to_string(),
file_size: 10000,
media_type: "image/png".to_string(), media_type: "image/png".to_string(),
}; };
let object_id = "https://example.org/emojis/test"; let object_id = "https://example.org/emojis/test";
@ -156,6 +157,7 @@ mod tests {
let db_client = &create_test_database().await; let db_client = &create_test_database().await;
let image = EmojiImage { let image = EmojiImage {
file_name: "test.png".to_string(), file_name: "test.png".to_string(),
file_size: 10000,
media_type: "image/png".to_string(), media_type: "image/png".to_string(),
}; };
let emoji = create_emoji( let emoji = create_emoji(

View file

@ -4,10 +4,15 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::database::json_macro::{json_from_sql, json_to_sql}; use crate::database::json_macro::{json_from_sql, json_to_sql};
use super::validators::EMOJI_MAX_SIZE;
fn default_emoji_file_size() -> usize { EMOJI_MAX_SIZE }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmojiImage { pub struct EmojiImage {
pub file_name: String, pub file_name: String,
#[serde(default = "default_emoji_file_size")]
pub file_size: usize,
pub media_type: String, pub media_type: String,
} }

View file

@ -34,16 +34,19 @@ use super::validators::{
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ProfileImage { pub struct ProfileImage {
pub file_name: String, pub file_name: String,
pub file_size: Option<usize>,
pub media_type: Option<String>, pub media_type: Option<String>,
} }
impl ProfileImage { impl ProfileImage {
pub fn new( pub fn new(
file_name: String, file_name: String,
file_size: usize,
media_type: Option<String>, media_type: Option<String>,
) -> Self { ) -> Self {
Self { Self {
file_name, file_name,
file_size: Some(file_size),
media_type, media_type,
} }
} }