Save sizes of media attachments and other files to database
This commit is contained in:
parent
e3b51d0752
commit
b958b8fb4c
14 changed files with 66 additions and 27 deletions
|
@ -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.
|
||||||
|
|
1
migrations/V0040__media_attachment__add_file_size.sql
Normal file
1
migrations/V0040__media_attachment__add_file_size.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE media_attachment ADD COLUMN file_size INTEGER;
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
¤t_user.id,
|
¤t_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(
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue