Fix various issues (#524)

* Fix various issues

* Fix multipart body hang

* drop req if error

* Make multipart errors more helpful
This commit is contained in:
Geometrically
2023-01-16 16:45:19 -07:00
committed by GitHub
parent 1679a3f844
commit 867ba7b68f
7 changed files with 427 additions and 357 deletions

View File

@@ -320,7 +320,7 @@ impl User {
id as UserId, id as UserId,
) )
.fetch_many(&mut *transaction) .fetch_many(&mut *transaction)
.try_filter_map(|e| async { Ok(e.right().map(|m| m.id as i64)) }) .try_filter_map(|e| async { Ok(e.right().map(|m| m.id)) })
.try_collect::<Vec<i64>>() .try_collect::<Vec<i64>>()
.await?; .await?;
@@ -442,7 +442,7 @@ impl User {
id as UserId, id as UserId,
) )
.fetch_many(&mut *transaction) .fetch_many(&mut *transaction)
.try_filter_map(|e| async { Ok(e.right().map(|m| m.id as i64)) }) .try_filter_map(|e| async { Ok(e.right().map(|m| m.id)) })
.try_collect::<Vec<i64>>() .try_collect::<Vec<i64>>()
.await?; .await?;

View File

@@ -21,7 +21,7 @@ pub async fn connect() -> Result<PgPool, sqlx::Error> {
.and_then(|x| x.parse().ok()) .and_then(|x| x.parse().ok())
.unwrap_or(16), .unwrap_or(16),
) )
.max_lifetime(Some(Duration::from_secs(60 * 60))) .max_lifetime(Some(Duration::from_secs(60 * 60 * 6)))
.connect(&database_url) .connect(&database_url)
.await?; .await?;

View File

@@ -97,7 +97,9 @@ impl PayoutsQueue {
})?; })?;
} }
let fee = if payout.recipient_wallet == *"Venmo" { let wallet = payout.recipient_wallet.clone();
let fee = if wallet == *"Venmo" {
Decimal::ONE / Decimal::from(4) Decimal::ONE / Decimal::from(4)
} else { } else {
std::cmp::min( std::cmp::min(
@@ -111,6 +113,7 @@ impl PayoutsQueue {
}; };
payout.amount.value -= fee; payout.amount.value -= fee;
payout.amount.value = payout.amount.value.round_dp(2);
if payout.amount.value <= Decimal::ZERO { if payout.amount.value <= Decimal::ZERO {
return Err(ApiError::InvalidInput( return Err(ApiError::InvalidInput(
@@ -153,7 +156,7 @@ impl PayoutsQueue {
"Error while registering payment in PayPal: {}", "Error while registering payment in PayPal: {}",
body.body.message body.body.message
))); )));
} else { } else if wallet != *"Venmo" {
#[derive(Deserialize)] #[derive(Deserialize)]
struct PayPalLink { struct PayPalLink {
href: String, href: String,

View File

@@ -11,14 +11,14 @@ use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ARError { pub enum ARError {
/// Read/Write error on store /// Read/Write error on store
#[error("read/write operatiion failed: {0}")] #[error("read/write operation failed: {0}")]
ReadWrite(String), ReadWrite(String),
/// Identifier error /// Identifier error
#[error("client identification failed")] #[error("client identification failed")]
Identification, Identification,
/// Limited Error /// Limited Error
#[error("You are being ratelimited. Please wait {reset} seconds. {remaining}/{max_requests} remaining.")] #[error("You are being rate-limited. Please wait {reset} seconds. {remaining}/{max_requests} remaining.")]
Limited { Limited {
max_requests: usize, max_requests: usize,
remaining: usize, remaining: usize,

View File

@@ -11,7 +11,6 @@ use crate::search::indexing::IndexingError;
use crate::util::auth::{get_user_from_headers, AuthenticationError}; use crate::util::auth::{get_user_from_headers, AuthenticationError};
use crate::util::routes::read_from_field; use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use actix::fut::ready;
use actix_multipart::{Field, Multipart}; use actix_multipart::{Field, Multipart};
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::web::Data; use actix_web::web::Data;
@@ -36,8 +35,8 @@ pub enum CreateError {
DatabaseError(#[from] models::DatabaseError), DatabaseError(#[from] models::DatabaseError),
#[error("Indexing Error: {0}")] #[error("Indexing Error: {0}")]
IndexingError(#[from] IndexingError), IndexingError(#[from] IndexingError),
#[error("Error while parsing multipart payload")] #[error("Error while parsing multipart payload: {0}")]
MultipartError(actix_multipart::MultipartError), MultipartError(#[from] actix_multipart::MultipartError),
#[error("Error while parsing JSON: {0}")] #[error("Error while parsing JSON: {0}")]
SerDeError(#[from] serde_json::Error), SerDeError(#[from] serde_json::Error),
#[error("Error while validating input: {0}")] #[error("Error while validating input: {0}")]
@@ -287,9 +286,6 @@ pub async fn project_create(
let undo_result = undo_uploads(&***file_host, &uploaded_files).await; let undo_result = undo_uploads(&***file_host, &uploaded_files).await;
let rollback_result = transaction.rollback().await; let rollback_result = transaction.rollback().await;
// fix multipart error bug:
payload.for_each(|_| ready(())).await;
undo_result?; undo_result?;
if let Err(e) = rollback_result { if let Err(e) = rollback_result {
return Err(e.into()); return Err(e.into());
@@ -475,128 +471,153 @@ pub async fn project_create_inner(
let mut icon_data = None; let mut icon_data = None;
let mut error = None;
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
let mut field: Field = item.map_err(CreateError::MultipartError)?; let mut field: Field = item?;
let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| { if error.is_some() {
CreateError::MissingValueError("Missing content name".to_string())
})?;
let (file_name, file_extension) =
super::version_creation::get_name_ext(&content_disposition)?;
if name == "icon" {
if icon_data.is_some() {
return Err(CreateError::InvalidInput(String::from(
"Projects can only have one icon",
)));
}
// Upload the icon to the cdn
icon_data = Some(
process_icon_upload(
uploaded_files,
project_id,
file_extension,
file_host,
field,
&cdn_url,
)
.await?,
);
continue; continue;
} }
if let Some(gallery_items) = &project_create_data.gallery_items { let result = async {
if gallery_items.iter().filter(|a| a.featured).count() > 1 { let content_disposition = field.content_disposition().clone();
return Err(CreateError::InvalidInput(String::from(
"Only one gallery image can be featured.", let name = content_disposition.get_name().ok_or_else(|| {
))); CreateError::MissingValueError(
"Missing content name".to_string(),
)
})?;
let (file_name, file_extension) =
super::version_creation::get_name_ext(&content_disposition)?;
if name == "icon" {
if icon_data.is_some() {
return Err(CreateError::InvalidInput(String::from(
"Projects can only have one icon",
)));
}
// Upload the icon to the cdn
icon_data = Some(
process_icon_upload(
uploaded_files,
project_id,
file_extension,
file_host,
field,
&cdn_url,
)
.await?,
);
return Ok(());
} }
if let Some(item) = gallery_items.iter().find(|x| x.item == name) { if let Some(gallery_items) = &project_create_data.gallery_items {
let data = read_from_field( if gallery_items.iter().filter(|a| a.featured).count() > 1 {
&mut field, return Err(CreateError::InvalidInput(String::from(
5 * (1 << 20), "Only one gallery image can be featured.",
"Gallery image exceeds the maximum of 5MiB.", )));
) }
.await?;
let hash = sha1::Sha1::from(&data).hexdigest(); if let Some(item) =
let (_, file_extension) = gallery_items.iter().find(|x| x.item == name)
super::version_creation::get_name_ext( {
&content_disposition, let data = read_from_field(
)?; &mut field,
let content_type = 5 * (1 << 20),
crate::util::ext::get_image_content_type(file_extension) "Gallery image exceeds the maximum of 5MiB.",
)
.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(|| { .ok_or_else(|| {
CreateError::InvalidIconFormat( CreateError::InvalidIconFormat(
file_extension.to_string(), file_extension.to_string(),
) )
})?; })?;
let url = format!( let url = format!(
"data/{}/images/{}.{}", "data/{}/images/{}.{}",
project_id, hash, file_extension project_id, hash, file_extension
); );
let upload_data = file_host let upload_data = file_host
.upload_file(content_type, &url, data.freeze()) .upload_file(content_type, &url, data.freeze())
.await?; .await?;
uploaded_files.push(UploadedFile { uploaded_files.push(UploadedFile {
file_id: upload_data.file_id, file_id: upload_data.file_id,
file_name: upload_data.file_name.clone(), file_name: upload_data.file_name,
}); });
gallery_urls.push(crate::models::projects::GalleryItem { gallery_urls.push(crate::models::projects::GalleryItem {
url: format!("{}/{}", cdn_url, url), url: format!("{}/{}", cdn_url, url),
featured: item.featured, featured: item.featured,
title: item.title.clone(), title: item.title.clone(),
description: item.description.clone(), description: item.description.clone(),
created: Utc::now(), created: Utc::now(),
ordering: item.ordering, ordering: item.ordering,
}); });
continue; return Ok(());
}
} }
let index = if let Some(i) = versions_map.get(name) {
*i
} else {
return Err(CreateError::InvalidInput(format!(
"File `{}` (field {}) isn't specified in the versions data",
file_name, name
)));
};
// `index` is always valid for these lists
let created_version = versions.get_mut(index).unwrap();
let version_data =
project_create_data.initial_versions.get(index).unwrap();
// Upload the new jar file
super::version_creation::upload_file(
&mut field,
file_host,
version_data.file_parts.len(),
uploaded_files,
&mut created_version.files,
&mut created_version.dependencies,
&cdn_url,
&content_disposition,
project_id,
created_version.version_id.into(),
&project_create_data.project_type,
version_data.loaders.clone(),
version_data.game_versions.clone(),
all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
None,
transaction,
)
.await?;
Ok(())
} }
.await;
let index = if let Some(i) = versions_map.get(name) { if result.is_err() {
*i error = result.err();
} else { }
return Err(CreateError::InvalidInput(format!( }
"File `{}` (field {}) isn't specified in the versions data",
file_name, name
)));
};
// `index` is always valid for these lists if let Some(error) = error {
let created_version = versions.get_mut(index).unwrap(); return Err(error);
let version_data =
project_create_data.initial_versions.get(index).unwrap();
// Upload the new jar file
super::version_creation::upload_file(
&mut field,
file_host,
version_data.file_parts.len(),
uploaded_files,
&mut created_version.files,
&mut created_version.dependencies,
&cdn_url,
&content_disposition,
project_id,
created_version.version_id.into(),
&project_create_data.project_type,
version_data.loaders.clone(),
version_data.game_versions.clone(),
all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
None,
transaction,
)
.await?;
} }
{ {

View File

@@ -15,7 +15,6 @@ use crate::util::auth::get_user_from_headers;
use crate::util::routes::read_from_field; use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use crate::validate::{validate_file, ValidationResult}; use crate::validate::{validate_file, ValidationResult};
use actix::fut::ready;
use actix_multipart::{Field, Multipart}; use actix_multipart::{Field, Multipart};
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{post, web, HttpRequest, HttpResponse}; use actix_web::{post, web, HttpRequest, HttpResponse};
@@ -101,8 +100,6 @@ pub async fn version_create(
.await; .await;
let rollback_result = transaction.rollback().await; let rollback_result = transaction.rollback().await;
payload.for_each(|_| ready(())).await;
undo_result?; undo_result?;
if let Err(e) = rollback_result { if let Err(e) = rollback_result {
return Err(e.into()); return Err(e.into());
@@ -133,207 +130,235 @@ async fn version_create_inner(
let user = get_user_from_headers(req.headers(), &mut *transaction).await?; let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
let mut error = None;
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
let mut field: Field = item.map_err(CreateError::MultipartError)?; let mut field: Field = item?;
let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError("Missing content name".to_string())
})?;
if name == "data" { if error.is_some() {
let mut data = Vec::new(); continue;
while let Some(chunk) = field.next().await { }
data.extend_from_slice(
&chunk.map_err(CreateError::MultipartError)?,
);
}
let version_create_data: InitialVersionData = let result = async {
serde_json::from_slice(&data)?; let content_disposition = field.content_disposition().clone();
initial_version_data = Some(version_create_data); let name = content_disposition.get_name().ok_or_else(|| {
let version_create_data = initial_version_data.as_ref().unwrap(); CreateError::MissingValueError(
if version_create_data.project_id.is_none() { "Missing content name".to_string(),
return Err(CreateError::MissingValueError(
"Missing project id".to_string(),
));
}
version_create_data.validate().map_err(|err| {
CreateError::ValidationError(validation_errors_to_string(
err, None,
))
})?;
if !version_create_data.status.can_be_requested() {
return Err(CreateError::InvalidInput(
"Status specified cannot be requested".to_string(),
));
}
let project_id: models::ProjectId =
version_create_data.project_id.unwrap().into();
// Ensure that the project this version is being added to exists
let results = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
project_id as models::ProjectId
)
.fetch_one(&mut *transaction)
.await?;
if !results.exists.unwrap_or(false) {
return Err(CreateError::InvalidInput(
"An invalid project id was supplied".to_string(),
));
}
// Check that the user creating this version is a team member
// of the project the version is being added to.
let team_member = models::TeamMember::get_from_user_id_project(
project_id,
user.id.into(),
&mut *transaction,
)
.await?
.ok_or_else(|| {
CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!"
.to_string(),
) )
})?; })?;
if !team_member if name == "data" {
.permissions let mut data = Vec::new();
.contains(Permissions::UPLOAD_VERSION) while let Some(chunk) = field.next().await {
{ data.extend_from_slice(&chunk?);
return Err(CreateError::CustomAuthenticationError( }
"You don't have permission to upload this version!"
.to_string(),
));
}
let version_id: VersionId = let version_create_data: InitialVersionData =
models::generate_version_id(transaction).await?.into(); serde_json::from_slice(&data)?;
initial_version_data = Some(version_create_data);
let version_create_data =
initial_version_data.as_ref().unwrap();
if version_create_data.project_id.is_none() {
return Err(CreateError::MissingValueError(
"Missing project id".to_string(),
));
}
let project_type = sqlx::query!( version_create_data.validate().map_err(|err| {
" CreateError::ValidationError(validation_errors_to_string(
err, None,
))
})?;
if !version_create_data.status.can_be_requested() {
return Err(CreateError::InvalidInput(
"Status specified cannot be requested".to_string(),
));
}
let project_id: models::ProjectId =
version_create_data.project_id.unwrap().into();
// Ensure that the project this version is being added to exists
let results = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
project_id as models::ProjectId
)
.fetch_one(&mut *transaction)
.await?;
if !results.exists.unwrap_or(false) {
return Err(CreateError::InvalidInput(
"An invalid project id was supplied".to_string(),
));
}
// Check that the user creating this version is a team member
// of the project the version is being added to.
let team_member = models::TeamMember::get_from_user_id_project(
project_id,
user.id.into(),
&mut *transaction,
)
.await?
.ok_or_else(|| {
CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!"
.to_string(),
)
})?;
if !team_member
.permissions
.contains(Permissions::UPLOAD_VERSION)
{
return Err(CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!"
.to_string(),
));
}
let version_id: VersionId =
models::generate_version_id(transaction).await?.into();
let project_type = sqlx::query!(
"
SELECT name FROM project_types pt SELECT name FROM project_types pt
INNER JOIN mods ON mods.project_type = pt.id INNER JOIN mods ON mods.project_type = pt.id
WHERE mods.id = $1 WHERE mods.id = $1
", ",
project_id as models::ProjectId, project_id as models::ProjectId,
)
.fetch_one(&mut *transaction)
.await?
.name;
let game_versions = version_create_data
.game_versions
.iter()
.map(|x| {
all_game_versions
.iter()
.find(|y| y.version == x.0)
.ok_or_else(|| {
CreateError::InvalidGameVersion(x.0.clone())
})
.map(|y| y.id)
})
.collect::<Result<Vec<models::GameVersionId>, CreateError>>(
)?;
let loaders = version_create_data
.loaders
.iter()
.map(|x| {
all_loaders
.iter()
.find(|y| {
y.loader == x.0
&& y.supported_project_types
.contains(&project_type)
})
.ok_or_else(|| {
CreateError::InvalidLoader(x.0.clone())
})
.map(|y| y.id)
})
.collect::<Result<Vec<models::LoaderId>, CreateError>>()?;
let dependencies = version_create_data
.dependencies
.iter()
.map(|d| models::version_item::DependencyBuilder {
version_id: d.version_id.map(|x| x.into()),
project_id: d.project_id.map(|x| x.into()),
dependency_type: d.dependency_type.to_string(),
file_name: None,
})
.collect::<Vec<_>>();
version_builder = Some(VersionBuilder {
version_id: version_id.into(),
project_id,
author_id: user.id.into(),
name: version_create_data.version_title.clone(),
version_number: version_create_data.version_number.clone(),
changelog: version_create_data
.version_body
.clone()
.unwrap_or_default(),
files: Vec::new(),
dependencies,
game_versions,
loaders,
version_type: version_create_data
.release_channel
.to_string(),
featured: version_create_data.featured,
status: version_create_data.status,
requested_status: None,
});
return Ok(());
}
let version = version_builder.as_mut().ok_or_else(|| {
CreateError::InvalidInput(String::from(
"`data` field must come before file fields",
))
})?;
let project_type = sqlx::query!(
"
SELECT name FROM project_types pt
INNER JOIN mods ON mods.project_type = pt.id
WHERE mods.id = $1
",
version.project_id as models::ProjectId,
) )
.fetch_one(&mut *transaction) .fetch_one(&mut *transaction)
.await? .await?
.name; .name;
let game_versions = version_create_data let version_data =
.game_versions initial_version_data.clone().ok_or_else(|| {
.iter() CreateError::InvalidInput(
.map(|x| { "`data` field is required".to_string(),
all_game_versions )
.iter() })?;
.find(|y| y.version == x.0)
.ok_or_else(|| {
CreateError::InvalidGameVersion(x.0.clone())
})
.map(|y| y.id)
})
.collect::<Result<Vec<models::GameVersionId>, CreateError>>()?;
let loaders = version_create_data upload_file(
.loaders &mut field,
.iter() file_host,
.map(|x| { version_data.file_parts.len(),
all_loaders uploaded_files,
.iter() &mut version.files,
.find(|y| { &mut version.dependencies,
y.loader == x.0 &cdn_url,
&& y.supported_project_types &content_disposition,
.contains(&project_type) version.project_id.into(),
}) version.version_id.into(),
.ok_or_else(|| CreateError::InvalidLoader(x.0.clone())) &project_type,
.map(|y| y.id) version_data.loaders,
}) version_data.game_versions,
.collect::<Result<Vec<models::LoaderId>, CreateError>>()?; all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
version_data.file_types.get(name).copied().flatten(),
transaction,
)
.await?;
let dependencies = version_create_data Ok(())
.dependencies
.iter()
.map(|d| models::version_item::DependencyBuilder {
version_id: d.version_id.map(|x| x.into()),
project_id: d.project_id.map(|x| x.into()),
dependency_type: d.dependency_type.to_string(),
file_name: None,
})
.collect::<Vec<_>>();
version_builder = Some(VersionBuilder {
version_id: version_id.into(),
project_id,
author_id: user.id.into(),
name: version_create_data.version_title.clone(),
version_number: version_create_data.version_number.clone(),
changelog: version_create_data
.version_body
.clone()
.unwrap_or_default(),
files: Vec::new(),
dependencies,
game_versions,
loaders,
version_type: version_create_data.release_channel.to_string(),
featured: version_create_data.featured,
status: version_create_data.status,
requested_status: None,
});
continue;
} }
.await;
let version = version_builder.as_mut().ok_or_else(|| { if result.is_err() {
CreateError::InvalidInput(String::from( error = result.err();
"`data` field must come before file fields", }
)) }
})?;
let project_type = sqlx::query!( if let Some(error) = error {
" return Err(error);
SELECT name FROM project_types pt
INNER JOIN mods ON mods.project_type = pt.id
WHERE mods.id = $1
",
version.project_id as models::ProjectId,
)
.fetch_one(&mut *transaction)
.await?
.name;
let version_data = initial_version_data.clone().ok_or_else(|| {
CreateError::InvalidInput("`data` field is required".to_string())
})?;
upload_file(
&mut field,
file_host,
version_data.file_parts.len(),
uploaded_files,
&mut version.files,
&mut version.dependencies,
&cdn_url,
&content_disposition,
version.project_id.into(),
version.version_id.into(),
&project_type,
version_data.loaders,
version_data.game_versions,
all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
version_data.file_types.get(name).copied().flatten(),
transaction,
)
.await?;
} }
let version_data = initial_version_data.ok_or_else(|| { let version_data = initial_version_data.ok_or_else(|| {
@@ -479,8 +504,6 @@ pub async fn upload_file_to_version(
.await; .await;
let rollback_result = transaction.rollback().await; let rollback_result = transaction.rollback().await;
payload.for_each(|_| ready(())).await;
undo_result?; undo_result?;
if let Err(e) = rollback_result { if let Err(e) = rollback_result {
return Err(e.into()); return Err(e.into());
@@ -562,69 +585,88 @@ async fn upload_file_to_version_inner(
let all_game_versions = let all_game_versions =
models::categories::GameVersion::list(&mut *transaction).await?; models::categories::GameVersion::list(&mut *transaction).await?;
let mut error = None;
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
let mut field: Field = item.map_err(CreateError::MultipartError)?; let mut field: Field = item?;
let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError("Missing content name".to_string())
})?;
if name == "data" { if error.is_some() {
let mut data = Vec::new();
while let Some(chunk) = field.next().await {
data.extend_from_slice(
&chunk.map_err(CreateError::MultipartError)?,
);
}
let file_data: InitialFileData = serde_json::from_slice(&data)?;
initial_file_data = Some(file_data);
continue; continue;
} }
let file_data = initial_file_data.as_ref().ok_or_else(|| { let result = async {
CreateError::InvalidInput(String::from( let content_disposition = field.content_disposition().clone();
"`data` field must come before file fields", let name = content_disposition.get_name().ok_or_else(|| {
)) CreateError::MissingValueError(
})?; "Missing content name".to_string(),
)
})?;
let mut dependencies = version if name == "data" {
.dependencies let mut data = Vec::new();
.iter() while let Some(chunk) = field.next().await {
.map(|x| DependencyBuilder { data.extend_from_slice(&chunk?);
project_id: x.project_id, }
version_id: x.version_id, let file_data: InitialFileData = serde_json::from_slice(&data)?;
file_name: x.file_name.clone(),
dependency_type: x.dependency_type.clone(),
})
.collect();
upload_file( initial_file_data = Some(file_data);
&mut field, return Ok(());
file_host, }
0,
uploaded_files, let file_data = initial_file_data.as_ref().ok_or_else(|| {
&mut file_builders, CreateError::InvalidInput(String::from(
&mut dependencies, "`data` field must come before file fields",
&cdn_url, ))
&content_disposition, })?;
project_id,
version_id.into(), let mut dependencies = version
&project_type, .dependencies
version.loaders.clone().into_iter().map(Loader).collect(), .iter()
version .map(|x| DependencyBuilder {
.game_versions project_id: x.project_id,
.clone() version_id: x.version_id,
.into_iter() file_name: x.file_name.clone(),
.map(GameVersion) dependency_type: x.dependency_type.clone(),
.collect(), })
all_game_versions.clone(), .collect();
true,
false, upload_file(
file_data.file_types.get(name).copied().flatten(), &mut field,
transaction, file_host,
) 0,
.await?; uploaded_files,
&mut file_builders,
&mut dependencies,
&cdn_url,
&content_disposition,
project_id,
version_id.into(),
&project_type,
version.loaders.clone().into_iter().map(Loader).collect(),
version
.game_versions
.clone()
.into_iter()
.map(GameVersion)
.collect(),
all_game_versions.clone(),
true,
false,
file_data.file_types.get(name).copied().flatten(),
transaction,
)
.await?;
Ok(())
}
.await;
if result.is_err() {
error = result.err();
}
}
if let Some(error) = error {
return Err(error);
} }
if file_builders.is_empty() { if file_builders.is_empty() {
@@ -665,6 +707,12 @@ pub async fn upload_file(
) -> Result<(), CreateError> { ) -> Result<(), CreateError> {
let (file_name, file_extension) = get_name_ext(content_disposition)?; let (file_name, file_extension) = get_name_ext(content_disposition)?;
if file_name.contains('/') {
return Err(CreateError::InvalidInput(
"File names must not contain slashes!".to_string(),
));
}
let content_type = crate::util::ext::project_file_type(file_extension) let content_type = crate::util::ext::project_file_type(file_extension)
.ok_or_else(|| { .ok_or_else(|| {
CreateError::InvalidFileType(file_extension.to_string()) CreateError::InvalidFileType(file_extension.to_string())

View File

@@ -37,9 +37,7 @@ pub async fn read_from_field(
if bytes.len() >= cap { if bytes.len() >= cap {
return Err(CreateError::InvalidInput(String::from(err_msg))); return Err(CreateError::InvalidInput(String::from(err_msg)));
} else { } else {
bytes.extend_from_slice( bytes.extend_from_slice(&chunk?);
&chunk.map_err(CreateError::MultipartError)?,
);
} }
} }
Ok(bytes) Ok(bytes)