Optimize user-generated images for reduced bandwidth (#961)

* Optimize user-generated images for reduced bandwidth

* run prepare

* Finish compression
This commit is contained in:
Geometrically
2024-09-07 17:44:49 -07:00
committed by GitHub
parent cb0f03ca9c
commit 5b5599128a
51 changed files with 1306 additions and 1016 deletions
+24 -25
View File
@@ -14,7 +14,8 @@ use crate::routes::internal::session::issue_session;
use crate::routes::ApiError;
use crate::util::captcha::check_turnstile_captcha;
use crate::util::env::parse_strings_from_var;
use crate::util::ext::{get_image_content_type, get_image_ext};
use crate::util::ext::get_image_ext;
use crate::util::img::upload_image_optimized;
use crate::util::validate::{validation_errors_to_string, RE_URL_SAFE};
use actix_web::web::{scope, Data, Payload, Query, ServiceConfig};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
@@ -112,9 +113,7 @@ impl TempUser {
}
}
let avatar_url = if let Some(avatar_url) = self.avatar_url {
let cdn_url = dotenvy::var("CDN_URL")?;
let (avatar_url, raw_avatar_url) = if let Some(avatar_url) = self.avatar_url {
let res = reqwest::get(&avatar_url).await?;
let headers = res.headers().clone();
@@ -122,36 +121,34 @@ impl TempUser {
.get(reqwest::header::CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
{
get_image_ext(content_type).map(|ext| (ext, content_type))
} else if let Some(ext) = avatar_url.rsplit('.').next() {
get_image_content_type(ext).map(|content_type| (ext, content_type))
get_image_ext(content_type)
} else {
None
avatar_url.rsplit('.').next()
};
if let Some((ext, content_type)) = img_data {
if let Some(ext) = img_data {
let bytes = res.bytes().await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let upload_data = file_host
.upload_file(
content_type,
&format!(
"user/{}/{}.{}",
crate::models::users::UserId::from(user_id),
hash,
ext
),
bytes,
)
.await?;
let upload_result = upload_image_optimized(
&format!("user/{}", crate::models::users::UserId::from(user_id)),
bytes,
ext,
Some(96),
Some(1.0),
&**file_host,
)
.await;
Some(format!("{}/{}", cdn_url, upload_data.file_name))
if let Ok(upload_result) = upload_result {
(Some(upload_result.url), Some(upload_result.raw_url))
} else {
(None, None)
}
} else {
None
(None, None)
}
} else {
None
(None, None)
};
if let Some(username) = username {
@@ -223,6 +220,7 @@ impl TempUser {
email: self.email,
email_verified: true,
avatar_url,
raw_avatar_url,
bio: self.bio,
created: Utc::now(),
role: Role::Developer.to_string(),
@@ -1518,6 +1516,7 @@ pub async fn create_account_with_password(
email: Some(new_account.email.clone()),
email_verified: false,
avatar_url: None,
raw_avatar_url: None,
bio: None,
created: Utc::now(),
role: Role::Developer.to_string(),
+68 -79
View File
@@ -10,6 +10,7 @@ use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::CreateError;
use crate::routes::ApiError;
use crate::util::img::delete_old_images;
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use crate::{database, models};
@@ -371,78 +372,69 @@ pub async fn collection_icon_edit(
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::COLLECTION_WRITE]),
)
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::COLLECTION_WRITE]),
)
.await?
.1;
let string = info.into_inner().0;
let id = database::models::CollectionId(parse_base62(&string)? as i64);
let collection_item = database::models::Collection::get(id, &**pool, &redis)
.await?
.1;
.ok_or_else(|| {
ApiError::InvalidInput("The specified collection does not exist!".to_string())
})?;
let string = info.into_inner().0;
let id = database::models::CollectionId(parse_base62(&string)? as i64);
let collection_item = database::models::Collection::get(id, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified collection does not exist!".to_string())
})?;
if !can_modify_collection(&collection_item, &user) {
return Ok(HttpResponse::Unauthorized().body(""));
}
if let Some(icon) = collection_item.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let color = crate::util::img::get_color_from_img(&bytes)?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let collection_id: CollectionId = collection_item.id.into();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/{}.{}", collection_id, hash, ext.ext),
bytes.freeze(),
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE collections
SET icon_url = $1, color = $2
WHERE (id = $3)
",
format!("{}/{}", cdn_url, upload_data.file_name),
color.map(|x| x as i32),
collection_item.id as database::models::ids::CollectionId,
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(format!(
"Invalid format for collection icon: {}",
ext.ext
)))
if !can_modify_collection(&collection_item, &user) {
return Ok(HttpResponse::Unauthorized().body(""));
}
delete_old_images(
collection_item.icon_url,
collection_item.raw_icon_url,
&***file_host,
)
.await?;
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let collection_id: CollectionId = collection_item.id.into();
let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{}", collection_id),
bytes.freeze(),
&ext.ext,
Some(96),
Some(1.0),
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE collections
SET icon_url = $1, raw_icon_url = $2, color = $3
WHERE (id = $4)
",
upload_result.url,
upload_result.raw_url,
upload_result.color.map(|x| x as i32),
collection_item.id as database::models::ids::CollectionId,
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
Ok(HttpResponse::NoContent().body(""))
}
pub async fn delete_collection_icon(
@@ -474,21 +466,18 @@ pub async fn delete_collection_icon(
return Ok(HttpResponse::Unauthorized().body(""));
}
let cdn_url = dotenvy::var("CDN_URL")?;
if let Some(icon) = collection_item.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
delete_old_images(
collection_item.icon_url,
collection_item.raw_icon_url,
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE collections
SET icon_url = NULL, color = NULL
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
WHERE (id = $1)
",
collection_item.id as database::models::ids::CollectionId,
+169 -185
View File
@@ -1,5 +1,6 @@
use std::sync::Arc;
use super::threads::is_authorized_thread;
use crate::auth::checks::{is_team_member_project, is_team_member_version};
use crate::auth::get_user_from_headers;
use crate::database;
@@ -11,13 +12,12 @@ use crate::models::images::{Image, ImageContext};
use crate::models::reports::ReportId;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::img::upload_image_optimized;
use crate::util::routes::read_from_payload;
use actix_web::{web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use super::threads::is_authorized_thread;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("image", web::post().to(images_add));
}
@@ -46,198 +46,182 @@ pub async fn images_add(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&data.ext) {
let mut context = ImageContext::from_str(&data.context, None);
let mut context = ImageContext::from_str(&data.context, None);
let scopes = vec![context.relevant_scope()];
let scopes = vec![context.relevant_scope()];
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, Some(&scopes))
.await?
.1;
let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, Some(&scopes))
.await?
.1;
// Attempt to associated a supplied id with the context
// If the context cannot be found, or the user is not authorized to upload images for the context, return an error
match &mut context {
ImageContext::Project { project_id } => {
if let Some(id) = data.project_id {
let project = project_item::Project::get(&id, &**pool, &redis).await?;
if let Some(project) = project {
if is_team_member_project(&project.inner, &Some(user.clone()), &pool)
.await?
{
*project_id = Some(project.inner.id.into());
} else {
return Err(ApiError::CustomAuthentication(
"You are not authorized to upload images for this project"
.to_string(),
));
}
} else {
return Err(ApiError::InvalidInput(
"The project could not be found.".to_string(),
));
}
}
}
ImageContext::Version { version_id } => {
if let Some(id) = data.version_id {
let version = version_item::Version::get(id.into(), &**pool, &redis).await?;
if let Some(version) = version {
if is_team_member_version(
&version.inner,
&Some(user.clone()),
&pool,
&redis,
)
.await?
{
*version_id = Some(version.inner.id.into());
} else {
return Err(ApiError::CustomAuthentication(
"You are not authorized to upload images for this version"
.to_string(),
));
}
} else {
return Err(ApiError::InvalidInput(
"The version could not be found.".to_string(),
));
}
}
}
ImageContext::ThreadMessage { thread_message_id } => {
if let Some(id) = data.thread_message_id {
let thread_message = thread_item::ThreadMessage::get(id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The thread message could not found.".to_string(),
)
})?;
let thread = thread_item::Thread::get(thread_message.thread_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The thread associated with the thread message could not be found"
.to_string(),
)
})?;
if is_authorized_thread(&thread, &user, &pool).await? {
*thread_message_id = Some(thread_message.id.into());
// Attempt to associated a supplied id with the context
// If the context cannot be found, or the user is not authorized to upload images for the context, return an error
match &mut context {
ImageContext::Project { project_id } => {
if let Some(id) = data.project_id {
let project = project_item::Project::get(&id, &**pool, &redis).await?;
if let Some(project) = project {
if is_team_member_project(&project.inner, &Some(user.clone()), &pool).await? {
*project_id = Some(project.inner.id.into());
} else {
return Err(ApiError::CustomAuthentication(
"You are not authorized to upload images for this thread message"
.to_string(),
"You are not authorized to upload images for this project".to_string(),
));
}
} else {
return Err(ApiError::InvalidInput(
"The project could not be found.".to_string(),
));
}
}
ImageContext::Report { report_id } => {
if let Some(id) = data.report_id {
let report = report_item::Report::get(id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The report could not be found.".to_string())
})?;
let thread = thread_item::Thread::get(report.thread_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The thread associated with the report could not be found."
.to_string(),
)
})?;
if is_authorized_thread(&thread, &user, &pool).await? {
*report_id = Some(report.id.into());
} else {
return Err(ApiError::CustomAuthentication(
"You are not authorized to upload images for this report".to_string(),
));
}
}
}
ImageContext::Unknown => {
return Err(ApiError::InvalidInput(
"Context must be one of: project, version, thread_message, report".to_string(),
));
}
}
// Upload the image to the file host
let bytes =
read_from_payload(&mut payload, 1_048_576, "Icons must be smaller than 1MiB").await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/cached_images/{}.{}", hash, data.ext),
bytes.freeze(),
)
.await?;
let mut transaction = pool.begin().await?;
let db_image: database::models::Image = database::models::Image {
id: database::models::generate_image_id(&mut transaction).await?,
url: format!("{}/{}", cdn_url, upload_data.file_name),
size: upload_data.content_length as u64,
created: chrono::Utc::now(),
owner_id: database::models::UserId::from(user.id),
context: context.context_as_str().to_string(),
project_id: if let ImageContext::Project {
project_id: Some(id),
} = context
{
Some(database::models::ProjectId::from(id))
} else {
None
},
version_id: if let ImageContext::Version {
version_id: Some(id),
} = context
{
Some(database::models::VersionId::from(id))
} else {
None
},
thread_message_id: if let ImageContext::ThreadMessage {
thread_message_id: Some(id),
} = context
{
Some(database::models::ThreadMessageId::from(id))
} else {
None
},
report_id: if let ImageContext::Report {
report_id: Some(id),
} = context
{
Some(database::models::ReportId::from(id))
} else {
None
},
};
// Insert
db_image.insert(&mut transaction).await?;
let image = Image {
id: db_image.id.into(),
url: db_image.url,
size: db_image.size,
created: db_image.created,
owner_id: db_image.owner_id.into(),
context,
};
transaction.commit().await?;
Ok(HttpResponse::Ok().json(image))
} else {
Err(ApiError::InvalidInput(
"The specified file is not an image!".to_string(),
))
ImageContext::Version { version_id } => {
if let Some(id) = data.version_id {
let version = version_item::Version::get(id.into(), &**pool, &redis).await?;
if let Some(version) = version {
if is_team_member_version(&version.inner, &Some(user.clone()), &pool, &redis)
.await?
{
*version_id = Some(version.inner.id.into());
} else {
return Err(ApiError::CustomAuthentication(
"You are not authorized to upload images for this version".to_string(),
));
}
} else {
return Err(ApiError::InvalidInput(
"The version could not be found.".to_string(),
));
}
}
}
ImageContext::ThreadMessage { thread_message_id } => {
if let Some(id) = data.thread_message_id {
let thread_message = thread_item::ThreadMessage::get(id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The thread message could not found.".to_string())
})?;
let thread = thread_item::Thread::get(thread_message.thread_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The thread associated with the thread message could not be found"
.to_string(),
)
})?;
if is_authorized_thread(&thread, &user, &pool).await? {
*thread_message_id = Some(thread_message.id.into());
} else {
return Err(ApiError::CustomAuthentication(
"You are not authorized to upload images for this thread message"
.to_string(),
));
}
}
}
ImageContext::Report { report_id } => {
if let Some(id) = data.report_id {
let report = report_item::Report::get(id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The report could not be found.".to_string())
})?;
let thread = thread_item::Thread::get(report.thread_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The thread associated with the report could not be found.".to_string(),
)
})?;
if is_authorized_thread(&thread, &user, &pool).await? {
*report_id = Some(report.id.into());
} else {
return Err(ApiError::CustomAuthentication(
"You are not authorized to upload images for this report".to_string(),
));
}
}
}
ImageContext::Unknown => {
return Err(ApiError::InvalidInput(
"Context must be one of: project, version, thread_message, report".to_string(),
));
}
}
// Upload the image to the file host
let bytes =
read_from_payload(&mut payload, 1_048_576, "Icons must be smaller than 1MiB").await?;
let content_length = bytes.len();
let upload_result = upload_image_optimized(
"data/cached_images",
bytes.freeze(),
&data.ext,
None,
None,
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
let db_image: database::models::Image = database::models::Image {
id: database::models::generate_image_id(&mut transaction).await?,
url: upload_result.url,
raw_url: upload_result.raw_url,
size: content_length as u64,
created: chrono::Utc::now(),
owner_id: database::models::UserId::from(user.id),
context: context.context_as_str().to_string(),
project_id: if let ImageContext::Project {
project_id: Some(id),
} = context
{
Some(crate::database::models::ProjectId::from(id))
} else {
None
},
version_id: if let ImageContext::Version {
version_id: Some(id),
} = context
{
Some(database::models::VersionId::from(id))
} else {
None
},
thread_message_id: if let ImageContext::ThreadMessage {
thread_message_id: Some(id),
} = context
{
Some(database::models::ThreadMessageId::from(id))
} else {
None
},
report_id: if let ImageContext::Report {
report_id: Some(id),
} = context
{
Some(database::models::ReportId::from(id))
} else {
None
},
};
// Insert
db_image.insert(&mut transaction).await?;
let image = Image {
id: db_image.id.into(),
url: db_image.url,
size: db_image.size,
created: db_image.created,
owner_id: db_image.owner_id.into(),
context,
};
transaction.commit().await?;
Ok(HttpResponse::Ok().json(image))
}
+51 -63
View File
@@ -42,6 +42,7 @@ use crate::{
use crate::database::models::oauth_client_item::OAuthClient as DBOAuthClient;
use crate::models::ids::OAuthClientId as ApiOAuthClientId;
use crate::util::img::{delete_old_images, upload_image_optimized};
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
@@ -135,12 +136,6 @@ pub struct NewOAuthApp {
)]
pub name: String,
#[validate(
custom(function = "crate::util::validate::validate_url"),
length(max = 255)
)]
pub icon_url: Option<String>,
#[validate(custom(function = "crate::util::validate::validate_no_restricted_scopes"))]
pub max_scopes: Scopes,
@@ -190,7 +185,8 @@ pub async fn oauth_client_create<'a>(
let client = OAuthClient {
id: client_id,
icon_url: new_oauth_app.icon_url.clone(),
icon_url: None,
raw_icon_url: None,
max_scopes: new_oauth_app.max_scopes,
name: new_oauth_app.name.clone(),
redirect_uris,
@@ -349,63 +345,56 @@ pub async fn oauth_client_icon_edit(
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_ACCESS]),
)
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_ACCESS]),
)
.await?
.1;
let client = OAuthClient::get((*client_id).into(), &**pool)
.await?
.1;
.ok_or_else(|| {
ApiError::InvalidInput("The specified client does not exist!".to_string())
})?;
let client = OAuthClient::get((*client_id).into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified client does not exist!".to_string())
})?;
client.validate_authorized(Some(&user))?;
client.validate_authorized(Some(&user))?;
delete_old_images(
client.icon_url.clone(),
client.raw_icon_url.clone(),
&***file_host,
)
.await?;
if let Some(ref icon) = client.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let upload_result = upload_image_optimized(
&format!("data/{}", client_id),
bytes.freeze(),
&ext.ext,
Some(96),
Some(1.0),
&***file_host,
)
.await?;
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
let mut transaction = pool.begin().await?;
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/{}.{}", client_id, hash, ext.ext),
bytes.freeze(),
)
.await?;
let mut editable_client = client.clone();
editable_client.icon_url = Some(upload_result.url);
editable_client.raw_icon_url = Some(upload_result.raw_url);
let mut transaction = pool.begin().await?;
editable_client
.update_editable_fields(&mut *transaction)
.await?;
let mut editable_client = client.clone();
editable_client.icon_url = Some(format!("{}/{}", cdn_url, upload_data.file_name));
transaction.commit().await?;
editable_client
.update_editable_fields(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(format!(
"Invalid format for project icon: {}",
ext.ext
)))
}
Ok(HttpResponse::NoContent().body(""))
}
#[delete("app/{id}/icon")]
@@ -417,7 +406,6 @@ pub async fn oauth_client_icon_delete(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
@@ -435,18 +423,18 @@ pub async fn oauth_client_icon_delete(
})?;
client.validate_authorized(Some(&user))?;
if let Some(ref icon) = client.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
delete_old_images(
client.icon_url.clone(),
client.raw_icon_url.clone(),
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
let mut editable_client = client.clone();
editable_client.icon_url = None;
editable_client.raw_icon_url = None;
editable_client
.update_editable_fields(&mut *transaction)
+86 -95
View File
@@ -14,6 +14,7 @@ use crate::models::pats::Scopes;
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::CreateError;
use crate::util::img::delete_old_images;
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use crate::{database, models};
@@ -164,6 +165,7 @@ pub async fn organization_create(
description: new_organization.description.clone(),
team_id,
icon_url: None,
raw_icon_url: None,
color: None,
};
organization.clone().insert(&mut transaction).await?;
@@ -926,98 +928,89 @@ pub async fn organization_icon_edit(
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::ORGANIZATION_WRITE]),
)
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::ORGANIZATION_WRITE]),
)
.await?
.1;
let string = info.into_inner().0;
let organization_item = database::models::Organization::get(&string, &**pool, &redis)
.await?
.1;
let string = info.into_inner().0;
.ok_or_else(|| {
ApiError::InvalidInput("The specified organization does not exist!".to_string())
})?;
let organization_item = database::models::Organization::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified organization does not exist!".to_string())
})?;
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
organization_item.team_id,
user.id.into(),
&**pool,
)
.await
.map_err(ApiError::Database)?;
let permissions =
OrganizationPermissions::get_permissions_by_role(&user.role, &team_member)
.unwrap_or_default();
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this organization's icon.".to_string(),
));
}
}
if let Some(icon) = organization_item.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let color = crate::util::img::get_color_from_img(&bytes)?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let organization_id: OrganizationId = organization_item.id.into();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/{}.{}", organization_id, hash, ext.ext),
bytes.freeze(),
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE organizations
SET icon_url = $1, color = $2
WHERE (id = $3)
",
format!("{}/{}", cdn_url, upload_data.file_name),
color.map(|x| x as i32),
organization_item.id as database::models::ids::OrganizationId,
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
organization_item.team_id,
user.id.into(),
&**pool,
)
.execute(&mut *transaction)
.await?;
.await
.map_err(ApiError::Database)?;
transaction.commit().await?;
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.slug),
&redis,
)
.await?;
let permissions =
OrganizationPermissions::get_permissions_by_role(&user.role, &team_member)
.unwrap_or_default();
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(format!(
"Invalid format for project icon: {}",
ext.ext
)))
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this organization's icon.".to_string(),
));
}
}
delete_old_images(
organization_item.icon_url,
organization_item.raw_icon_url,
&***file_host,
)
.await?;
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let organization_id: OrganizationId = organization_item.id.into();
let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{}", organization_id),
bytes.freeze(),
&ext.ext,
Some(96),
Some(1.0),
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE organizations
SET icon_url = $1, raw_icon_url = $2, color = $3
WHERE (id = $4)
",
upload_result.url,
upload_result.raw_url,
upload_result.color.map(|x| x as i32),
organization_item.id as database::models::ids::OrganizationId,
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.slug),
&redis,
)
.await?;
Ok(HttpResponse::NoContent().body(""))
}
pub async fn delete_organization_icon(
@@ -1065,21 +1058,19 @@ pub async fn delete_organization_icon(
}
}
let cdn_url = dotenvy::var("CDN_URL")?;
if let Some(icon) = organization_item.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
delete_old_images(
organization_item.icon_url,
organization_item.raw_icon_url,
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE organizations
SET icon_url = NULL, color = NULL
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
WHERE (id = $1)
",
organization_item.id as database::models::ids::OrganizationId,
+51 -38
View File
@@ -6,6 +6,7 @@ use crate::database::models::{self, image_item, User};
use crate::database::redis::RedisPool;
use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError;
use crate::models::ids::base62_impl::to_base62;
use crate::models::ids::{ImageId, OrganizationId};
use crate::models::images::{Image, ImageContext};
use crate::models::pats::Scopes;
@@ -17,6 +18,7 @@ use crate::models::threads::ThreadType;
use crate::models::users::UserId;
use crate::queue::session::AuthQueue;
use crate::search::indexing::IndexingError;
use crate::util::img::upload_image_optimized;
use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string;
use actix_multipart::{Field, Multipart};
@@ -481,7 +483,6 @@ async fn project_create_inner(
file_extension,
file_host,
field,
&cdn_url,
)
.await?,
);
@@ -496,33 +497,40 @@ async fn project_create_inner(
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
let data = read_from_field(
&mut field,
5 * (1 << 20),
"Gallery image exceeds the maximum of 5MiB.",
2 * (1 << 20),
"Gallery image exceeds the maximum of 2MiB.",
)
.await?;
let hash = sha1::Sha1::from(&data).hexdigest();
let (_, file_extension) =
super::version_creation::get_name_ext(&content_disposition)?;
let content_type = crate::util::ext::get_image_content_type(file_extension)
.ok_or_else(|| {
CreateError::InvalidIconFormat(file_extension.to_string())
})?;
let url = format!("data/{project_id}/images/{hash}.{file_extension}");
let upload_data = file_host
.upload_file(content_type, &url, data.freeze())
.await?;
let url = format!("data/{project_id}/images");
let upload_result = upload_image_optimized(
&url,
data.freeze(),
file_extension,
Some(350),
Some(1.0),
file_host,
)
.await
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
uploaded_files.push(UploadedFile {
file_id: upload_data.file_id,
file_name: upload_data.file_name,
file_id: upload_result.raw_url_path.clone(),
file_name: upload_result.raw_url_path,
});
gallery_urls.push(crate::models::projects::GalleryItem {
url: format!("{cdn_url}/{url}"),
url: upload_result.url,
raw_url: upload_result.raw_url,
featured: item.featured,
name: item.name.clone(),
description: item.description.clone(),
created: Utc::now(),
ordering: item.ordering,
});
return Ok(());
}
}
@@ -715,6 +723,7 @@ async fn project_create_inner(
summary: project_create_data.summary,
description: project_create_data.description,
icon_url: icon_data.clone().map(|x| x.0),
raw_icon_url: icon_data.clone().map(|x| x.1),
license_url: project_create_data.license_url,
categories,
@@ -729,6 +738,7 @@ async fn project_create_inner(
.iter()
.map(|x| models::project_item::GalleryItem {
image_url: x.url.clone(),
raw_image_url: x.raw_url.clone(),
featured: x.featured,
name: x.name.clone(),
description: x.description.clone(),
@@ -736,7 +746,7 @@ async fn project_create_inner(
ordering: x.ordering,
})
.collect(),
color: icon_data.and_then(|x| x.1),
color: icon_data.and_then(|x| x.2),
monetization_status: MonetizationStatus::Monetized,
};
let project_builder = project_builder_actual.clone();
@@ -943,29 +953,32 @@ async fn process_icon_upload(
file_extension: &str,
file_host: &dyn FileHost,
mut field: Field,
cdn_url: &str,
) -> Result<(String, Option<u32>), CreateError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
) -> Result<(String, String, Option<u32>), CreateError> {
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{}", to_base62(id)),
data.freeze(),
file_extension,
Some(96),
Some(1.0),
file_host,
)
.await
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
let color = crate::util::img::get_color_from_img(&data)?;
uploaded_files.push(UploadedFile {
file_id: upload_result.raw_url_path.clone(),
file_name: upload_result.raw_url_path,
});
let hash = sha1::Sha1::from(&data).hexdigest();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{id}/{hash}.{file_extension}"),
data.freeze(),
)
.await?;
uploaded_files.push(UploadedFile {
file_id: upload_result.url_path.clone(),
file_name: upload_result.url_path,
});
uploaded_files.push(UploadedFile {
file_id: upload_data.file_id,
file_name: upload_data.file_name.clone(),
});
Ok((format!("{}/{}", cdn_url, upload_data.file_name), color))
} else {
Err(CreateError::InvalidIconFormat(file_extension.to_string()))
}
Ok((
upload_result.url,
upload_result.raw_url,
upload_result.color,
))
}
+199 -225
View File
@@ -26,6 +26,7 @@ use crate::routes::ApiError;
use crate::search::indexing::remove_documents;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::img;
use crate::util::img::{delete_old_images, upload_image_optimized};
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use actix_web::{web, HttpRequest, HttpResponse};
@@ -1317,109 +1318,95 @@ pub async fn project_icon_edit(
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
.await?
.1;
let string = info.into_inner().0;
let project_item = db_models::Project::get(&string, &**pool, &redis)
.await?
.1;
let string = info.into_inner().0;
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
let project_item = db_models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !user.role.is_mod() {
let (team_member, organization_team_member) =
db_models::TeamMember::get_for_project_permissions(
&project_item.inner,
user.id.into(),
&**pool,
)
.await?;
// Hide the project
if team_member.is_none() && organization_team_member.is_none() {
return Err(ApiError::CustomAuthentication(
"The specified project does not exist!".to_string(),
));
}
let permissions = ProjectPermissions::get_permissions_by_role(
&user.role,
&team_member,
&organization_team_member,
)
.unwrap_or_default();
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon.".to_string(),
));
}
}
if let Some(icon) = project_item.inner.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let color = crate::util::img::get_color_from_img(&bytes)?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let project_id: ProjectId = project_item.inner.id.into();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/{}.{}", project_id, hash, ext.ext),
bytes.freeze(),
if !user.role.is_mod() {
let (team_member, organization_team_member) =
db_models::TeamMember::get_for_project_permissions(
&project_item.inner,
user.id.into(),
&**pool,
)
.await?;
let mut transaction = pool.begin().await?;
// Hide the project
if team_member.is_none() && organization_team_member.is_none() {
return Err(ApiError::CustomAuthentication(
"The specified project does not exist!".to_string(),
));
}
sqlx::query!(
"
UPDATE mods
SET icon_url = $1, color = $2
WHERE (id = $3)
",
format!("{}/{}", cdn_url, upload_data.file_name),
color.map(|x| x as i32),
project_item.inner.id as db_ids::ProjectId,
let permissions = ProjectPermissions::get_permissions_by_role(
&user.role,
&team_member,
&organization_team_member,
)
.execute(&mut *transaction)
.await?;
.unwrap_or_default();
transaction.commit().await?;
db_models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(format!(
"Invalid format for project icon: {}",
ext.ext
)))
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon.".to_string(),
));
}
}
delete_old_images(
project_item.inner.icon_url,
project_item.inner.raw_icon_url,
&***file_host,
)
.await?;
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let project_id: ProjectId = project_item.inner.id.into();
let upload_result = upload_image_optimized(
&format!("data/{}", project_id),
bytes.freeze(),
&ext.ext,
Some(96),
Some(1.0),
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE mods
SET icon_url = $1, raw_icon_url = $2, color = $3
WHERE (id = $4)
",
upload_result.url,
upload_result.raw_url,
upload_result.color.map(|x| x as i32),
project_item.inner.id as db_ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis)
.await?;
Ok(HttpResponse::NoContent().body(""))
}
pub async fn delete_project_icon(
@@ -1476,21 +1463,19 @@ pub async fn delete_project_icon(
}
}
let cdn_url = dotenvy::var("CDN_URL")?;
if let Some(icon) = project_item.inner.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
delete_old_images(
project_item.inner.icon_url,
project_item.inner.raw_icon_url,
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE mods
SET icon_url = NULL, color = NULL
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
WHERE (id = $1)
",
project_item.inner.id as db_ids::ProjectId,
@@ -1527,132 +1512,122 @@ pub async fn add_gallery_item(
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
item.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
item.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
.await?
.1;
let string = info.into_inner().0;
let project_item = db_models::Project::get(&string, &**pool, &redis)
.await?
.1;
let string = info.into_inner().0;
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
let project_item = db_models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if project_item.gallery_items.len() > 64 {
return Err(ApiError::CustomAuthentication(
"You have reached the maximum of gallery images to upload.".to_string(),
));
}
if project_item.gallery_items.len() > 64 {
return Err(ApiError::CustomAuthentication(
"You have reached the maximum of gallery images to upload.".to_string(),
));
}
if !user.role.is_admin() {
let (team_member, organization_team_member) =
db_models::TeamMember::get_for_project_permissions(
&project_item.inner,
user.id.into(),
&**pool,
)
.await?;
// Hide the project
if team_member.is_none() && organization_team_member.is_none() {
return Err(ApiError::CustomAuthentication(
"The specified project does not exist!".to_string(),
));
}
let permissions = ProjectPermissions::get_permissions_by_role(
&user.role,
&team_member,
&organization_team_member,
if !user.role.is_admin() {
let (team_member, organization_team_member) =
db_models::TeamMember::get_for_project_permissions(
&project_item.inner,
user.id.into(),
&**pool,
)
.unwrap_or_default();
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery.".to_string(),
));
}
}
let bytes = read_from_payload(
&mut payload,
5 * (1 << 20),
"Gallery image exceeds the maximum of 5MiB.",
)
.await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let id: ProjectId = project_item.inner.id.into();
let url = format!("data/{}/images/{}.{}", id, hash, &*ext.ext);
let file_url = format!("{cdn_url}/{url}");
if project_item
.gallery_items
.iter()
.any(|x| x.image_url == file_url)
{
return Err(ApiError::InvalidInput(
"You may not upload duplicate gallery images!".to_string(),
));
}
file_host
.upload_file(content_type, &url, bytes.freeze())
.await?;
let mut transaction = pool.begin().await?;
// Hide the project
if team_member.is_none() && organization_team_member.is_none() {
return Err(ApiError::CustomAuthentication(
"The specified project does not exist!".to_string(),
));
}
if item.featured {
sqlx::query!(
"
let permissions = ProjectPermissions::get_permissions_by_role(
&user.role,
&team_member,
&organization_team_member,
)
.unwrap_or_default();
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery.".to_string(),
));
}
}
let bytes = read_from_payload(
&mut payload,
2 * (1 << 20),
"Gallery image exceeds the maximum of 2MiB.",
)
.await?;
let id: ProjectId = project_item.inner.id.into();
let upload_result = upload_image_optimized(
&format!("data/{}/images", id),
bytes.freeze(),
&ext.ext,
Some(350),
Some(1.0),
&***file_host,
)
.await?;
if project_item
.gallery_items
.iter()
.any(|x| x.image_url == upload_result.url)
{
return Err(ApiError::InvalidInput(
"You may not upload duplicate gallery images!".to_string(),
));
}
let mut transaction = pool.begin().await?;
if item.featured {
sqlx::query!(
"
UPDATE mods_gallery
SET featured = $2
WHERE mod_id = $1
",
project_item.inner.id as db_ids::ProjectId,
false,
)
.execute(&mut *transaction)
.await?;
}
let gallery_item = vec![db_models::project_item::GalleryItem {
image_url: file_url,
featured: item.featured,
name: item.name,
description: item.description,
created: Utc::now(),
ordering: item.ordering.unwrap_or(0),
}];
GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?;
transaction.commit().await?;
db_models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
project_item.inner.id as db_ids::ProjectId,
false,
)
.execute(&mut *transaction)
.await?;
}
let gallery_item = vec![db_models::project_item::GalleryItem {
image_url: upload_result.url,
raw_image_url: upload_result.raw_url,
featured: item.featured,
name: item.name,
description: item.description,
created: Utc::now(),
ordering: item.ordering.unwrap_or(0),
}];
GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?;
transaction.commit().await?;
db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis)
.await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(format!(
"Invalid format for gallery image: {}",
ext.ext
)))
}
Ok(HttpResponse::NoContent().body(""))
}
#[derive(Serialize, Deserialize, Validate)]
@@ -1891,9 +1866,9 @@ pub async fn delete_gallery_item(
}
let mut transaction = pool.begin().await?;
let id = sqlx::query!(
let item = sqlx::query!(
"
SELECT id FROM mods_gallery
SELECT id, image_url, raw_image_url FROM mods_gallery
WHERE image_url = $1
",
item.url
@@ -1905,15 +1880,14 @@ pub async fn delete_gallery_item(
"Gallery item at URL {} is not part of the project's gallery.",
item.url
))
})?
.id;
})?;
let cdn_url = dotenvy::var("CDN_URL")?;
let name = item.url.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
delete_old_images(
Some(item.image_url),
Some(item.raw_image_url),
&***file_host,
)
.await?;
let mut transaction = pool.begin().await?;
@@ -1922,7 +1896,7 @@ pub async fn delete_gallery_item(
DELETE FROM mods_gallery
WHERE id = $1
",
id
item.id
)
.execute(&mut *transaction)
.await?;
+55 -64
View File
@@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use validator::Validate;
use super::{oauth_clients::get_user_clients, ApiError};
use crate::util::img::delete_old_images;
use crate::{
auth::{filter_visible_projects, get_user_from_headers},
database::{models::User, redis::RedisPool},
@@ -23,8 +25,6 @@ use crate::{
util::{routes::read_from_payload, validate::validation_errors_to_string},
};
use super::{oauth_clients::get_user_clients, ApiError};
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("user", web::get().to(user_auth_get));
cfg.route("users", web::get().to(users_get));
@@ -446,71 +446,62 @@ pub async fn user_icon_edit(
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::USER_WRITE]),
)
.await?
.1;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::USER_WRITE]),
)
.await?
.1;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(actual_user) = id_option {
if user.id != actual_user.id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon.".to_string(),
));
}
let icon_url = actual_user.avatar_url;
let user_id: UserId = actual_user.id.into();
if let Some(icon) = icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
}
}
let bytes =
read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let upload_data = file_host
.upload_file(
content_type,
&format!("user/{}/{}.{}", user_id, hash, ext.ext),
bytes.freeze(),
)
.await?;
sqlx::query!(
"
UPDATE users
SET avatar_url = $1
WHERE (id = $2)
",
format!("{}/{}", cdn_url, upload_data.file_name),
actual_user.id as crate::database::models::ids::UserId,
)
.execute(&**pool)
.await?;
User::clear_caches(&[(actual_user.id, None)], &redis).await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::NotFound)
if let Some(actual_user) = id_option {
if user.id != actual_user.id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon.".to_string(),
));
}
delete_old_images(
actual_user.avatar_url,
actual_user.raw_avatar_url,
&***file_host,
)
.await?;
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let user_id: UserId = actual_user.id.into();
let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{}", user_id),
bytes.freeze(),
&ext.ext,
Some(96),
Some(1.0),
&***file_host,
)
.await?;
sqlx::query!(
"
UPDATE users
SET avatar_url = $1, raw_avatar_url = $2
WHERE (id = $3)
",
upload_result.url,
upload_result.raw_url,
actual_user.id as crate::database::models::ids::UserId,
)
.execute(&**pool)
.await?;
User::clear_caches(&[(actual_user.id, None)], &redis).await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(format!(
"Invalid format for user icon: {}",
ext.ext
)))
Err(ApiError::NotFound)
}
}