You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user