From 4073a7abc36fd1c1b9c4428e25ea325b2d8ad6f2 Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Sat, 21 Aug 2021 19:38:32 -0700 Subject: [PATCH] Force files to be unique, require all new versions to have at least one file (#236) --- sqlx-data.json | 41 ++++++++++++++++++++++++++++++++++ src/routes/project_creation.rs | 3 ++- src/routes/version_creation.rs | 38 ++++++++++++++++++++++++++++--- src/routes/version_file.rs | 20 +++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/sqlx-data.json b/sqlx-data.json index f6da124bc..0a29fd6ef 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1057,6 +1057,27 @@ "nullable": [] } }, + "4298552497a48adb9ace61c8dcf989c4d35866866b61c0cc4d45909b1d31c660": { + "query": "\n SELECT EXISTS(SELECT 1 FROM hashes h\n WHERE h.algorithm = $2 AND h.hash = $1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "436dbf448697436ec90c30f44b27c92ec626601e7a7a9edb4d11bd916741b60f": { "query": "\n UPDATE mods\n SET icon_url = NULL\n WHERE (id = $1)\n ", "describe": { @@ -5082,6 +5103,26 @@ ] } }, + "e29da865af4a0a110275b9756394546a3bb88bff40e18c66029651f515caed98": { + "query": "\n SELECT f.id id FROM files f\n WHERE f.version_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "e3235e872f98eb85d3eb4a2518fb9dc88049ce62362bfd02623e9b49ac2e9fed": { "query": "\n SELECT name FROM report_types\n ", "describe": { diff --git a/src/routes/project_creation.rs b/src/routes/project_creation.rs index c3bf087d2..d9392794c 100644 --- a/src/routes/project_creation.rs +++ b/src/routes/project_creation.rs @@ -289,7 +289,7 @@ Get logged in user pub async fn project_create_inner( req: HttpRequest, mut payload: Multipart, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, uploaded_files: &mut Vec, ) -> Result { @@ -512,6 +512,7 @@ pub async fn project_create_inner( version_data.game_versions.clone(), &all_game_versions, false, + &mut transaction, ) .await?; } diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index 350994934..472bdd098 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -87,7 +87,7 @@ pub async fn version_create( async fn version_create_inner( req: HttpRequest, mut payload: Multipart, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, uploaded_files: &mut Vec, ) -> Result { @@ -289,6 +289,7 @@ async fn version_create_inner( version_data.game_versions, &all_game_versions, false, + &mut transaction, ) .await?; } @@ -298,6 +299,12 @@ async fn version_create_inner( let builder = version_builder .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?; + if builder.files.is_empty() { + return Err(CreateError::InvalidInput( + "Versions must have at least one file uploaded to them".to_string(), + )); + } + let result = sqlx::query!( " SELECT m.title FROM mods m @@ -434,7 +441,7 @@ pub async fn upload_file_to_version( async fn upload_file_to_version_inner( req: HttpRequest, mut payload: Multipart, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, uploaded_files: &mut Vec, version_id: models::VersionId, @@ -536,6 +543,7 @@ async fn upload_file_to_version_inner( .collect(), &all_game_versions, true, + &mut transaction, ) .await?; } @@ -570,6 +578,7 @@ pub async fn upload_file( game_versions: Vec, all_game_versions: &[models::categories::GameVersion], ignore_primary: bool, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), CreateError> { let (file_name, file_extension) = get_name_ext(content_disposition)?; @@ -577,6 +586,7 @@ pub async fn upload_file( .ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?; let mut data = Vec::new(); + let mut hash = sha1::Sha1::new(); while let Some(chunk) = field.next().await { // Project file size limit of 100MiB const FILE_SIZE_CAP: usize = 100 * (1 << 20); @@ -586,10 +596,32 @@ pub async fn upload_file( String::from("Project file exceeds the maximum of 100MiB. Contact a moderator or admin to request permission to upload larger files.") )); } else { - data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); + let bytes = chunk.map_err(CreateError::MultipartError)?; + hash.update(&data); + data.append(&mut bytes.to_vec()); } } + let hash = hash.digest().to_string(); + let exists = sqlx::query!( + " + SELECT EXISTS(SELECT 1 FROM hashes h + WHERE h.algorithm = $2 AND h.hash = $1) + ", + hash.as_bytes(), + "sha1" + ) + .fetch_one(&mut *transaction) + .await? + .exists + .unwrap_or(false); + + if exists { + return Err(CreateError::InvalidInput( + "Duplicate files are not allowed to be uploaded to Modrinth!".to_string(), + )); + } + let validation_result = validate_file( data.as_slice(), file_extension, diff --git a/src/routes/version_file.rs b/src/routes/version_file.rs index 4ce7d559b..b7f4ffd87 100644 --- a/src/routes/version_file.rs +++ b/src/routes/version_file.rs @@ -240,6 +240,26 @@ pub async fn delete_file( } } + use futures::stream::TryStreamExt; + + let files = sqlx::query!( + " + SELECT f.id id FROM files f + WHERE f.version_id = $1 + ", + row.version_id + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { Ok(e.right().map(|_| ())) }) + .try_collect::>() + .await?; + + if files.len() < 2 { + return Err(ApiError::InvalidInputError( + "Versions must have at least one file uploaded to them".to_string(), + )); + } + let mut transaction = pool.begin().await?; sqlx::query!(