From 9ee92fb9e9b4453f8940af104783289c5133f802 Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Mon, 19 Jul 2021 11:30:39 -0700 Subject: [PATCH] Project gallery, webhook fixes, remove cache, re-enable donation URLs (#222) --- Cargo.lock | 86 +-- Cargo.toml | 2 - migrations/20210718223710_gallery.sql | 6 + sqlx-data.json | 905 ++++++++++++++------------ src/database/cache/mod.rs | 50 -- src/database/mod.rs | 1 - src/database/models/project_item.rs | 152 +++-- src/models/projects.rs | 3 + src/routes/project_creation.rs | 77 ++- src/routes/projects.rs | 320 +++++++-- src/routes/users.rs | 2 +- src/routes/version_creation.rs | 16 +- src/util/ext.rs | 27 + src/util/mod.rs | 1 + src/validate/mod.rs | 2 - 15 files changed, 961 insertions(+), 689 deletions(-) create mode 100644 migrations/20210718223710_gallery.sql delete mode 100644 src/database/cache/mod.rs create mode 100644 src/util/ext.rs diff --git a/Cargo.lock b/Cargo.lock index cf1f9680..bbb3f552 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,40 +827,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" -[[package]] -name = "cached" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2afe73808fbaac302e39c9754bfc3c4b4d0f99c9c240b9f4e4efc841ad1b74" -dependencies = [ - "async-mutex", - "async-trait", - "cached_proc_macro", - "cached_proc_macro_types", - "futures", - "hashbrown 0.9.1", - "once_cell", -] - -[[package]] -name = "cached_proc_macro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf857ae42d910aede5c5186e62684b0d7a597ce2fe3bd14448ab8f7ef439848c" -dependencies = [ - "async-mutex", - "cached_proc_macro_types", - "darling 0.10.2", - "quote", - "syn", -] - -[[package]] -name = "cached_proc_macro_types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" - [[package]] name = "cargo-platform" version = "0.1.1" @@ -1122,38 +1088,14 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - [[package]] name = "darling" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" dependencies = [ - "darling_core 0.12.4", - "darling_macro 0.12.4", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", + "darling_core", + "darling_macro", ] [[package]] @@ -1166,18 +1108,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core 0.10.2", - "quote", + "strsim", "syn", ] @@ -1187,7 +1118,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" dependencies = [ - "darling_core 0.12.4", + "darling_core", "quote", "syn", ] @@ -2026,7 +1957,6 @@ dependencies = [ "async-trait", "base64 0.13.0", "bitflags", - "cached", "chrono", "dotenv", "env_logger", @@ -3273,7 +3203,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48b35457e9d855d3dc05ef32a73e0df1e2c0fd72c38796a4ee909160c8eeec2" dependencies = [ - "darling 0.12.4", + "darling", "proc-macro2", "quote", "syn", @@ -3558,12 +3488,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "strsim" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index e6b6792f..d9e6492d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,5 +56,3 @@ sqlx = { version = "0.4.2", features = ["runtime-actix-rustls", "postgres", "chr sentry = { version = "0.22.0", features = ["log"] } sentry-actix = "0.22.0" - -cached = "0.23.0" \ No newline at end of file diff --git a/migrations/20210718223710_gallery.sql b/migrations/20210718223710_gallery.sql new file mode 100644 index 00000000..f578fd37 --- /dev/null +++ b/migrations/20210718223710_gallery.sql @@ -0,0 +1,6 @@ +-- Add migration script here +CREATE TABLE mods_gallery ( + id serial PRIMARY KEY, + mod_id bigint REFERENCES mods ON UPDATE CASCADE NOT NULL, + image_url varchar(2048) NOT NULL +); \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 533b2f4d..e4a0f66a 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1181,6 +1181,18 @@ "nullable": [] } }, + "436dbf448697436ec90c30f44b27c92ec626601e7a7a9edb4d11bd916741b60f": { + "query": "\n UPDATE mods\n SET icon_url = NULL\n WHERE (id = $1)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "43b793e2df30a6ace9e037e38bb4ea456656cfbe276c151e3a9e0a408d2c249f": { "query": "\n UPDATE versions\n SET release_channel = $1\n WHERE (id = $2)\n ", "describe": { @@ -1397,6 +1409,18 @@ ] } }, + "4d752ee3f43a1bf34d71c4391c9232537e0941294951f383ea8fa61e9d83fc96": { + "query": "\n DELETE FROM mods_gallery\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + } + }, "4e9f9eafbfd705dfc94571018cb747245a98ea61bad3fae4b3ce284229d99955": { "query": "\n UPDATE mods\n SET description = $1\n WHERE (id = $2)\n ", "describe": { @@ -2776,6 +2800,26 @@ ] } }, + "7c61fee015231f0a97c25d24f2c6be24821e39e330ab82344ad3b985d0d2aaea": { + "query": "\n SELECT id FROM mods_gallery\n WHERE image_url = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "7e73d3a17807f57ba6def5ff718e6dcb3a65ef8da653d839560b24635334cf05": { "query": "\n SELECT m.title FROM mods m\n WHERE id = $1\n ", "describe": { @@ -3650,212 +3694,6 @@ ] } }, - "adca565e97b4cdd8095c9ee56a449a8ecd7858489a5f82628201e7dfd30217d0": { - "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_type", - "type_info": "Int4" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 18, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 21, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "rejection_reason", - "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "rejection_body", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "status_name", - "type_info": "Varchar" - }, - { - "ordinal": 25, - "name": "client_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 26, - "name": "server_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 27, - "name": "short", - "type_info": "Varchar" - }, - { - "ordinal": 28, - "name": "license_name", - "type_info": "Varchar" - }, - { - "ordinal": 29, - "name": "project_type_name", - "type_info": "Varchar" - }, - { - "ordinal": 30, - "name": "categories", - "type_info": "Text" - }, - { - "ordinal": 31, - "name": "versions", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - false, - false, - null, - null - ] - } - }, "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { "query": "\n DELETE FROM teams\n WHERE id = $1\n ", "describe": { @@ -4659,212 +4497,6 @@ ] } }, - "caf24e9714afdea82cbfb8405cb291c8aee7c94bddc51260152ed1cbc629ffa1": { - "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "project_type", - "type_info": "Int4" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 18, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 21, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "rejection_reason", - "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "rejection_body", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "status_name", - "type_info": "Varchar" - }, - { - "ordinal": 25, - "name": "client_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 26, - "name": "server_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 27, - "name": "short", - "type_info": "Varchar" - }, - { - "ordinal": 28, - "name": "license_name", - "type_info": "Varchar" - }, - { - "ordinal": 29, - "name": "project_type_name", - "type_info": "Varchar" - }, - { - "ordinal": 30, - "name": "categories", - "type_info": "Text" - }, - { - "ordinal": 31, - "name": "versions", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - false, - false, - null, - null - ] - } - }, "cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8": { "query": "\n INSERT INTO hashes (file_id, algorithm, hash)\n VALUES ($1, $2, $3)\n ", "describe": { @@ -5383,6 +5015,224 @@ "nullable": [] } }, + "dc73655baea98436ddd0e266c7b123a9d240d4c1cb9d98875045e2235d772ab6": { + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url, ',') gallery,\n STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT OUTER JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT OUTER JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "rejection_reason", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "rejection_body", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 25, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 27, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 28, + "name": "license_name", + "type_info": "Varchar" + }, + { + "ordinal": 29, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 30, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 31, + "name": "versions", + "type_info": "Text" + }, + { + "ordinal": 32, + "name": "gallery", + "type_info": "Text" + }, + { + "ordinal": 33, + "name": "donations", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + false, + null, + null, + null, + null + ] + } + }, "e3235e872f98eb85d3eb4a2518fb9dc88049ce62362bfd02623e9b49ac2e9fed": { "query": "\n SELECT name FROM report_types\n ", "describe": { @@ -5458,6 +5308,224 @@ "nullable": [] } }, + "e74c46f568e202e4e8cd02935bdfe6bef860e174c1648a9d3d5ef653fc7ac983": { + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url, ',') gallery,\n STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT OUTER JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT OUTER JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "rejection_reason", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "rejection_body", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 25, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 27, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 28, + "name": "license_name", + "type_info": "Varchar" + }, + { + "ordinal": 29, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 30, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 31, + "name": "versions", + "type_info": "Text" + }, + { + "ordinal": 32, + "name": "gallery", + "type_info": "Text" + }, + { + "ordinal": 33, + "name": "donations", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + false, + null, + null, + null, + null + ] + } + }, "e7d0a64a08df6783c942f2fcadd94dd45f8d96ad3d3736e52ce90f68d396cdab": { "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)", "describe": { @@ -5531,6 +5599,19 @@ "nullable": [] } }, + "e9a79afea907cec2f6617e325b0b3b80d135ff99149a1516a8cffd4fbbd64e6d": { + "query": "\n INSERT INTO mods_gallery (\n mod_id, image_url\n )\n VALUES (\n $1, $2\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar" + ] + }, + "nullable": [] + } + }, "ea877d50ba461eae97ba3a35c3da71e7cdb7a92de1bb877d6b5dd766aca4e4ef": { "query": "\n SELECT u.id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role\n FROM users u\n WHERE u.github_id = $1\n ", "describe": { diff --git a/src/database/cache/mod.rs b/src/database/cache/mod.rs deleted file mode 100644 index c2a72186..00000000 --- a/src/database/cache/mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -//pub mod project_cache; -//pub mod project_query_cache; -#[macro_export] -macro_rules! generate_cache { - ($name:ident,$id:ty, $val:ty, $cache_name:ident, $mod_name:ident, $getter_name:ident, $setter_name:ident, $remover_name:ident) => { - pub mod $mod_name { - use cached::async_mutex::Mutex; - use cached::{Cached, SizedCache}; - use lazy_static::lazy_static; - lazy_static! { - static ref $cache_name: Mutex> = - Mutex::new(SizedCache::with_size(400)); - } - - pub async fn $getter_name<'a>(id: $id) -> Option<$val> { - let mut cache = $cache_name.lock().await; - Cached::cache_get(&mut *cache, &id).map(|e| e.clone()) - } - pub async fn $setter_name<'a>(id: $id, val: &$val) { - let mut cache = $cache_name.lock().await; - Cached::cache_set(&mut *cache, id, val.clone()); - } - pub async fn $remover_name<'a>(id: $id) { - let mut cache = $cache_name.lock().await; - Cached::cache_remove(&mut *cache, &id); - } - } - }; -} - -generate_cache!( - project, - String, - crate::database::Project, - PROJECT_CACHE, - project_cache, - get_cache_project, - set_cache_project, - remove_cache_project -); -generate_cache!( - query_project, - String, - crate::database::models::project_item::QueryProject, - QUERY_PROJECT_CACHE, - query_project_cache, - get_cache_query_project, - set_cache_query_project, - remove_cache_query_project -); diff --git a/src/database/mod.rs b/src/database/mod.rs index d321f146..6cda6f97 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,4 +1,3 @@ -pub mod cache; pub mod models; mod postgres_database; pub use models::Project; diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index c91703fc..40a1fd3a 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -1,8 +1,5 @@ use super::ids::*; -use crate::database::cache::project_cache::{get_cache_project, set_cache_project}; -use crate::database::cache::query_project_cache::{ - get_cache_query_project, set_cache_query_project, -}; + #[derive(Clone, Debug)] pub struct DonationUrl { pub project_id: ProjectId, @@ -37,6 +34,36 @@ impl DonationUrl { } } +#[derive(Clone, Debug)] +pub struct GalleryItem { + pub project_id: ProjectId, + pub image_url: String, +} + +impl GalleryItem { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), sqlx::error::Error> { + sqlx::query!( + " + INSERT INTO mods_gallery ( + mod_id, image_url + ) + VALUES ( + $1, $2 + ) + ", + self.project_id as ProjectId, + self.image_url, + ) + .execute(&mut *transaction) + .await?; + + Ok(()) + } +} + pub struct ProjectBuilder { pub project_id: ProjectId, pub project_type_id: ProjectTypeId, @@ -58,6 +85,7 @@ pub struct ProjectBuilder { pub license: LicenseId, pub slug: Option, pub donation_urls: Vec, + pub gallery_items: Vec, } impl ProjectBuilder { @@ -103,6 +131,11 @@ impl ProjectBuilder { donation.insert(&mut *transaction).await?; } + for mut gallery in self.gallery_items { + gallery.project_id = self.project_id; + gallery.insert(&mut *transaction).await?; + } + for category in self.categories { sqlx::query!( " @@ -491,11 +524,6 @@ impl Project { where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - // Check in the cache - let cached = get_cache_project(slug_or_project_id.clone()).await; - if let Some(data) = cached { - return Ok(Some(data)); - } let id_option = crate::models::ids::base62_impl::parse_base62(&*slug_or_project_id.clone()).ok(); @@ -505,22 +533,12 @@ impl Project { if project.is_none() { project = Project::get_from_slug(&slug_or_project_id, executor).await?; } - // Cache the response - if let Some(data) = project { - set_cache_project(slug_or_project_id.clone(), &data).await; - Ok(Some(data)) - } else { - Ok(None) - } + + Ok(project) } else { let project = Project::get_from_slug(&slug_or_project_id, executor).await?; - // Capture the data, and try to cache it - if let Some(data) = project { - set_cache_project(slug_or_project_id.clone(), &data).await; - Ok(Some(data)) - } else { - Ok(None) - } + + Ok(project) } } @@ -531,11 +549,6 @@ impl Project { where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - // Query cache - let cached = get_cache_query_project(slug_or_project_id.clone()).await; - if let Some(data) = cached { - return Ok(Some(data)); - } let id_option = crate::models::ids::base62_impl::parse_base62(&*slug_or_project_id.clone()).ok(); @@ -545,21 +558,11 @@ impl Project { if project.is_none() { project = Project::get_full_from_slug(&slug_or_project_id, executor).await?; } - // Save the variable - if let Some(data) = project { - set_cache_query_project(slug_or_project_id.clone(), &data).await; - Ok(Some(data)) - } else { - Ok(None) - } + + Ok(project) } else { let project = Project::get_full_from_slug(&slug_or_project_id, executor).await?; - if let Some(data) = project { - set_cache_query_project(slug_or_project_id.clone(), &data).await; - Ok(Some(data)) - } else { - Ok(None) - } + Ok(project) } } @@ -578,11 +581,15 @@ impl Project { m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body, s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, - STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions + STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url, ',') gallery, + STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations FROM mods m LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id LEFT OUTER JOIN versions v ON v.mod_id = m.id + LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id + LEFT OUTER JOIN mods_donations md ON md.joining_mod_id = m.id + LEFT OUTER JOIN donation_platforms dp ON md.joining_platform_id = dp.id INNER JOIN project_types pt ON pt.id = m.project_type INNER JOIN statuses s ON s.id = m.status INNER JOIN side_types cs ON m.client_side = cs.id @@ -637,7 +644,29 @@ impl Project { .split(',') .map(|x| VersionId(x.parse().unwrap_or_default())) .collect(), - donation_urls: vec![], + donation_urls: m + .donations + .unwrap_or_default() + .split(" ,") + .map(|d| { + let strings: Vec<&str> = d.split(", ").collect(); + DonationUrl { + project_id: id, + platform_id: DonationPlatformId(strings[0].parse().unwrap_or(0)), + platform_short: strings[2].to_string(), + platform_name: strings[3].to_string(), + url: strings[1].to_string(), + } + }) + .collect(), + gallery_items: m + .gallery + .into_iter() + .map(|x| GalleryItem { + project_id: id, + image_url: x, + }) + .collect(), status: crate::models::projects::ProjectStatus::from_str(&m.status_name), license_id: m.short, license_name: m.license_name, @@ -667,11 +696,15 @@ impl Project { m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body, s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, - STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions + STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url, ',') gallery, + STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations FROM mods m LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id LEFT OUTER JOIN versions v ON v.mod_id = m.id + LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id + LEFT OUTER JOIN mods_donations md ON md.joining_mod_id = m.id + LEFT OUTER JOIN donation_platforms dp ON md.joining_platform_id = dp.id INNER JOIN project_types pt ON pt.id = m.project_type INNER JOIN statuses s ON s.id = m.status INNER JOIN side_types cs ON m.client_side = cs.id @@ -684,9 +717,11 @@ impl Project { ) .fetch_many(exec) .try_filter_map(|e| async { - Ok(e.right().map(|m| QueryProject { - inner: Project { - id: ProjectId(m.id), + Ok(e.right().map(|m| { + let id = m.id; + QueryProject { + inner: Project { + id: ProjectId(id), project_type: ProjectTypeId(m.project_type), team_id: TeamId(m.team_id), title: m.title.clone(), @@ -714,13 +749,31 @@ impl Project { project_type: m.project_type_name, categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(), versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(), - donation_urls: vec![], + donation_urls: m + .donations + .unwrap_or_default() + .split(" ,") + .map(|d| { + let strings: Vec<&str> = d.split(", ").collect(); + DonationUrl { + project_id: ProjectId(id), + platform_id: DonationPlatformId(strings[0].parse().unwrap_or(0)), + platform_short: strings[2].to_string(), + platform_name: strings[3].to_string(), + url: strings[1].to_string(), + } + }) + .collect(), + gallery_items: m.gallery.iter().map(|x| GalleryItem { + project_id: ProjectId(id), + image_url: x.to_string() + }).collect(), status: crate::models::projects::ProjectStatus::from_str(&m.status_name), license_id: m.short, license_name: m.license_name, client_side: crate::models::projects::SideType::from_str(&m.client_side_type), server_side: crate::models::projects::SideType::from_str(&m.server_side_type), - })) + }})) }) .try_collect::>() .await @@ -733,6 +786,7 @@ pub struct QueryProject { pub categories: Vec, pub versions: Vec, pub donation_urls: Vec, + pub gallery_items: Vec, pub status: crate::models::projects::ProjectStatus, pub license_id: String, pub license_name: String, diff --git a/src/models/projects.rs b/src/models/projects.rs index 6cd9beac..16c99e27 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -75,6 +75,9 @@ pub struct Project { pub discord_url: Option, /// An optional list of all donation links the project has pub donation_urls: Option>, + + /// A string of URLs to visual content featuring the project + pub gallery: Vec, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/src/routes/project_creation.rs b/src/routes/project_creation.rs index 3b64ec30..8c006020 100644 --- a/src/routes/project_creation.rs +++ b/src/routes/project_creation.rs @@ -42,7 +42,7 @@ pub enum CreateError { FileValidationError(#[from] crate::validate::ValidationError), #[error("{}", .0)] MissingValueError(String), - #[error("Invalid format for project icon: {0}")] + #[error("Invalid format for image: {0}")] InvalidIconFormat(String), #[error("Error with multipart data: {0}")] InvalidInput(String), @@ -150,7 +150,7 @@ struct ProjectCreateData { /// The support range for the server project pub server_side: SideType, - #[validate(length(max = 64))] + #[validate(length(max = 32))] #[validate] /// A list of initial versions to upload with the created project pub initial_versions: Vec, @@ -182,6 +182,10 @@ struct ProjectCreateData { /// The license id that the project follows pub license_id: String, + + #[validate(length(max = 64))] + /// The multipart names of the gallery items to upload + pub gallery_items: Vec, } pub struct UploadedFile { @@ -285,6 +289,7 @@ pub async fn project_create_inner( let project_create_data; let mut versions; let mut versions_map = std::collections::HashMap::new(); + let mut gallery_urls = Vec::new(); let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; let all_loaders = models::categories::Loader::list(&mut *transaction).await?; @@ -421,6 +426,45 @@ pub async fn project_create_inner( continue; } + if project_create_data + .gallery_items + .iter() + .find(|x| *x == name) + .is_some() + { + let mut data = Vec::new(); + while let Some(chunk) = field.next().await { + data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); + } + + const FILE_SIZE_CAP: usize = 5 * (1 << 20); + + if data.len() >= FILE_SIZE_CAP { + return Err(CreateError::InvalidInput(String::from( + "Gallery image exceeds the maximum of 5MiB.", + ))); + } + + 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/{}/images/{}.{}", project_id, hash, file_extension); + let upload_data = file_host + .upload_file(content_type, &url, data.to_vec()) + .await?; + + uploaded_files.push(UploadedFile { + file_id: upload_data.file_id, + file_name: upload_data.file_name.clone(), + }); + + gallery_urls.push(format!("{}/{}", cdn_url, url)); + + continue; + } + let index = if let Some(i) = versions_map.get(name) { *i } else { @@ -578,6 +622,13 @@ pub async fn project_create_inner( license: license_id, slug: Some(project_create_data.slug), donation_urls, + gallery_items: gallery_urls + .iter() + .map(|x| models::project_item::GalleryItem { + project_id: project_id.into(), + image_url: x.to_string(), + }) + .collect(), }; let now = chrono::Utc::now(); @@ -616,6 +667,7 @@ pub async fn project_create_inner( wiki_url: project_builder.wiki_url.clone(), discord_url: project_builder.discord_url.clone(), donation_urls: project_create_data.donation_urls.clone(), + gallery: gallery_urls, }; let _project_id = project_builder.insert(&mut *transaction).await?; @@ -726,7 +778,7 @@ async fn process_icon_upload( mut field: actix_multipart::Field, cdn_url: &str, ) -> Result { - if let Some(content_type) = get_image_content_type(file_extension) { + if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) { let mut data = Vec::new(); while let Some(chunk) = field.next().await { data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); @@ -756,22 +808,3 @@ async fn process_icon_upload( Err(CreateError::InvalidIconFormat(file_extension.to_string())) } } - -pub fn get_image_content_type(extension: &str) -> Option<&'static str> { - let content_type = match &*extension { - "bmp" => "image/bmp", - "gif" => "image/gif", - "jpeg" | "jpg" | "jpe" => "image/jpeg", - "png" => "image/png", - "svg" | "svgz" => "image/svg+xml", - "webp" => "image/webp", - "rgb" => "image/x-rgb", - _ => "", - }; - - if !content_type.is_empty() { - Some(content_type) - } else { - None - } -} diff --git a/src/routes/projects.rs b/src/routes/projects.rs index 9530f744..a3e7e028 100644 --- a/src/routes/projects.rs +++ b/src/routes/projects.rs @@ -1,6 +1,4 @@ use crate::database; -use crate::database::cache::project_cache::remove_cache_project; -use crate::database::cache::query_project_cache::remove_cache_query_project; use crate::file_hosting::FileHost; use crate::models; use crate::models::projects::{ @@ -271,6 +269,11 @@ pub fn convert_project( }) .collect(), ), + gallery: data + .gallery_items + .into_iter() + .map(|x| x.image_url) + .collect(), } } @@ -452,33 +455,13 @@ pub async fn project_edit( )); } - if status == &ProjectStatus::Processing && project_item.versions.is_empty() { - return Err(ApiError::InvalidInputError(String::from( - "Project submitted for review with no initial versions", - ))); - } + if status == &ProjectStatus::Processing { + if project_item.versions.is_empty() { + return Err(ApiError::InvalidInputError(String::from( + "Project submitted for review with no initial versions", + ))); + } - let status_id = database::models::StatusId::get_id(&status, &mut *transaction) - .await? - .ok_or_else(|| { - ApiError::InvalidInputError( - "No database entry for status provided.".to_string(), - ) - })?; - - sqlx::query!( - " - UPDATE mods - SET status = $1 - WHERE (id = $2) - ", - status_id as database::models::ids::StatusId, - id as database::models::ids::ProjectId, - ) - .execute(&mut *transaction) - .await?; - - if project_item.status == ProjectStatus::Processing { sqlx::query!( " UPDATE mods @@ -511,6 +494,26 @@ pub async fn project_edit( } } + let status_id = database::models::StatusId::get_id(&status, &mut *transaction) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError( + "No database entry for status provided.".to_string(), + ) + })?; + + sqlx::query!( + " + UPDATE mods + SET status = $1 + WHERE (id = $2) + ", + status_id as database::models::ids::StatusId, + id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + if project_item.status.is_searchable() && !status.is_searchable() { delete_from_index(id.into(), config).await?; } else if !project_item.status.is_searchable() && status.is_searchable() { @@ -901,15 +904,6 @@ pub async fn project_edit( .await?; } - let id: ProjectId = project_item.inner.id.into(); - remove_cache_project(id.to_string().clone()).await; - remove_cache_query_project(id.to_string()).await; - - if let Some(slug) = project_item.inner.slug { - remove_cache_project(slug.clone()).await; - remove_cache_query_project(slug).await; - } - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -936,7 +930,7 @@ pub async fn project_icon_edit( file_host: web::Data>, mut payload: web::Payload, ) -> Result { - if let Some(content_type) = super::project_creation::get_image_content_type(&*ext.ext) { + if let Some(content_type) = crate::util::ext::get_image_content_type(&*ext.ext) { let cdn_url = dotenv::var("CDN_URL")?; let user = get_user_from_headers(req.headers(), &**pool).await?; let string = info.into_inner().0; @@ -988,7 +982,7 @@ pub async fn project_icon_edit( ))); } - let hash = sha1::Sha1::from(bytes.clone()).hexdigest(); + let hash = sha1::Sha1::from(&bytes).hexdigest(); let project_id: ProjectId = project_item.id.into(); @@ -1000,6 +994,8 @@ pub async fn project_icon_edit( ) .await?; + let mut transaction = pool.begin().await?; + sqlx::query!( " UPDATE mods @@ -1009,17 +1005,10 @@ pub async fn project_icon_edit( format!("{}/{}", cdn_url, upload_data.file_name), project_item.id as database::models::ids::ProjectId, ) - .execute(&**pool) + .execute(&mut *transaction) .await?; - let id: ProjectId = project_item.id.into(); - remove_cache_project(id.to_string().clone()).await; - remove_cache_query_project(id.to_string()).await; - - if let Some(slug) = project_item.slug { - remove_cache_project(slug.clone()).await; - remove_cache_query_project(slug).await; - } + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -1030,6 +1019,232 @@ pub async fn project_icon_edit( } } +#[delete("{id}/icon")] +pub async fn delete_project_icon( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + file_host: web::Data>, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let string = info.into_inner().0; + + let project_item = + database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; + + if !user.role.is_mod() { + let team_member = database::models::TeamMember::get_from_user_id( + project_item.team_id, + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; + + if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to edit this project's icon.".to_string(), + )); + } + } + + if let Some(icon) = project_item.icon_url { + let name = icon.split('/').next(); + + if let Some(icon_path) = name { + file_host.delete_file_version("", icon_path).await?; + } + } + + let mut transaction = pool.begin().await?; + + sqlx::query!( + " + UPDATE mods + SET icon_url = NULL + WHERE (id = $1) + ", + project_item.id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) +} + +#[post("{id}/gallery")] +pub async fn add_gallery_item( + web::Query(ext): web::Query, + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + file_host: web::Data>, + mut payload: web::Payload, +) -> Result { + if let Some(content_type) = crate::util::ext::get_image_content_type(&*ext.ext) { + let cdn_url = dotenv::var("CDN_URL")?; + let user = get_user_from_headers(req.headers(), &**pool).await?; + let string = info.into_inner().0; + + let project_item = + database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; + + if !user.role.is_mod() { + let team_member = database::models::TeamMember::get_from_user_id( + project_item.team_id, + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; + + if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to edit this project's gallery.".to_string(), + )); + } + } + + let mut bytes = web::BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item.map_err(|_| { + ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string()) + })?); + } + + const FILE_SIZE_CAP: usize = 5 * (1 << 20); + + if bytes.len() >= FILE_SIZE_CAP { + return Err(ApiError::InvalidInputError(String::from( + "Gallery image exceeds the maximum of 5MiB.", + ))); + } + + let hash = sha1::Sha1::from(&bytes).hexdigest(); + + let id: ProjectId = project_item.id.into(); + let url = format!("data/{}/images/{}.{}", id, hash, &*ext.ext); + file_host + .upload_file(content_type, &url, bytes.to_vec()) + .await?; + + let mut transaction = pool.begin().await?; + + database::models::project_item::GalleryItem { + project_id: project_item.id, + image_url: format!("{}/{}", cdn_url, url), + } + .insert(&mut transaction) + .await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Err(ApiError::InvalidInputError(format!( + "Invalid format for gallery image: {}", + ext.ext + ))) + } +} + +#[derive(Serialize, Deserialize)] +pub struct GalleryItem { + pub item: String, +} + +#[delete("{id}/gallery")] +pub async fn delete_gallery_item( + req: HttpRequest, + web::Query(item): web::Query, + info: web::Path<(String,)>, + pool: web::Data, + file_host: web::Data>, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let string = info.into_inner().0; + + let project_item = + database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; + + if !user.role.is_mod() { + let team_member = database::models::TeamMember::get_from_user_id( + project_item.team_id, + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; + + if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to edit this project's icon.".to_string(), + )); + } + } + let mut transaction = pool.begin().await?; + + let id = sqlx::query!( + " + SELECT id FROM mods_gallery + WHERE image_url = $1 + ", + item.item + ) + .fetch_optional(&mut *transaction) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError(format!( + "Gallery item at URL {} is not part of the project's gallery.", + item.item + )) + })? + .id; + + let name = item.item.split('/').next(); + + if let Some(item_path) = name { + file_host.delete_file_version("", item_path).await?; + } + + let mut transaction = pool.begin().await?; + + sqlx::query!( + " + DELETE FROM mods_gallery + WHERE id = $1 + ", + id + ) + .execute(&mut *transaction) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) +} + #[delete("{id}")] pub async fn project_delete( req: HttpRequest, @@ -1072,15 +1287,6 @@ pub async fn project_delete( let result = database::models::Project::remove_full(project.id, &mut transaction).await?; - let id: ProjectId = project.id.into(); - remove_cache_project(id.to_string().clone()).await; - remove_cache_query_project(id.to_string()).await; - - if let Some(slug) = project.slug { - remove_cache_project(slug.clone()).await; - remove_cache_query_project(slug).await; - } - transaction.commit().await?; delete_from_index(project.id.into(), config).await?; diff --git a/src/routes/users.rs b/src/routes/users.rs index 84187695..bc0b546e 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -286,7 +286,7 @@ pub async fn user_icon_edit( file_host: web::Data>, mut payload: web::Payload, ) -> Result { - if let Some(content_type) = super::project_creation::get_image_content_type(&*ext.ext) { + if let Some(content_type) = crate::util::ext::get_image_content_type(&*ext.ext) { let cdn_url = dotenv::var("CDN_URL")?; let user = get_user_from_headers(req.headers(), &**pool).await?; let id_option = diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index 8bf2d39d..5c50dcb0 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -580,7 +580,7 @@ pub async fn upload_file( ) -> Result<(), CreateError> { let (file_name, file_extension) = get_name_ext(content_disposition)?; - let content_type = project_file_type(file_extension) + let content_type = crate::util::ext::project_file_type(file_extension) .ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?; let mut data = Vec::new(); @@ -588,13 +588,13 @@ pub async fn upload_file( data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); } - // Project file size limit of 25MiB - const FILE_SIZE_CAP: usize = 25 * (2 << 30); + // Project file size limit of 100MiB + const FILE_SIZE_CAP: usize = 100 * (1 << 20); // TODO: override file size cap for authorized users or projects if data.len() >= FILE_SIZE_CAP { return Err(CreateError::InvalidInput( - String::from("Project file exceeds the maximum of 25MiB. Contact a moderator or admin to request permission to upload larger files.") + String::from("Project file exceeds the maximum of 100MiB. Contact a moderator or admin to request permission to upload larger files.") )); } @@ -649,14 +649,6 @@ pub async fn upload_file( Ok(()) } -// Currently we only support jar projects; this may change in the future (datapacks?) -fn project_file_type(ext: &str) -> Option<&str> { - match ext { - "jar" => Some("application/java-archive"), - _ => None, - } -} - pub fn get_name_ext( content_disposition: &actix_web::http::header::ContentDisposition, ) -> Result<(&str, &str), CreateError> { diff --git a/src/util/ext.rs b/src/util/ext.rs new file mode 100644 index 00000000..042e5847 --- /dev/null +++ b/src/util/ext.rs @@ -0,0 +1,27 @@ +pub fn get_image_content_type(extension: &str) -> Option<&'static str> { + let content_type = match &*extension { + "bmp" => "image/bmp", + "gif" => "image/gif", + "jpeg" | "jpg" | "jpe" => "image/jpeg", + "png" => "image/png", + "svg" | "svgz" => "image/svg+xml", + "webp" => "image/webp", + "rgb" => "image/x-rgb", + "mp4" => "video/mp4", + _ => "", + }; + + if !content_type.is_empty() { + Some(content_type) + } else { + None + } +} + +pub fn project_file_type(ext: &str) -> Option<&str> { + match ext { + "jar" => Some("application/java-archive"), + "zip" => Some("application/zip"), + _ => None, + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index ded21f6d..ac856955 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ pub mod auth; +pub mod ext; pub mod validate; pub mod webhook; diff --git a/src/validate/mod.rs b/src/validate/mod.rs index 9154a6aa..c7abef99 100644 --- a/src/validate/mod.rs +++ b/src/validate/mod.rs @@ -113,5 +113,3 @@ fn game_version_supported( } } } - -//todo: fabric/forge validators for 1.8+ respectively