Project gallery, webhook fixes, remove cache, re-enable donation URLs (#222)

This commit is contained in:
Geometrically
2021-07-19 11:30:39 -07:00
committed by GitHub
parent 981bf1d56f
commit 9ee92fb9e9
15 changed files with 961 additions and 689 deletions

86
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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
);

View File

@@ -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": {

View File

@@ -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<SizedCache<$id, $val>> =
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
);

View File

@@ -1,4 +1,3 @@
pub mod cache;
pub mod models;
mod postgres_database;
pub use models::Project;

View File

@@ -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<String>,
pub donation_urls: Vec<DonationUrl>,
pub gallery_items: Vec<GalleryItem>,
}
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::<Vec<QueryProject>>()
.await
@@ -733,6 +786,7 @@ pub struct QueryProject {
pub categories: Vec<String>,
pub versions: Vec<VersionId>,
pub donation_urls: Vec<DonationUrl>,
pub gallery_items: Vec<GalleryItem>,
pub status: crate::models::projects::ProjectStatus,
pub license_id: String,
pub license_name: String,

View File

@@ -75,6 +75,9 @@ pub struct Project {
pub discord_url: Option<String>,
/// An optional list of all donation links the project has
pub donation_urls: Option<Vec<DonationLink>>,
/// A string of URLs to visual content featuring the project
pub gallery: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View File

@@ -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<InitialVersionData>,
@@ -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<String>,
}
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<String, CreateError> {
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
}
}

View File

@@ -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<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, ApiError> {
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<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
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<GalleryItem>,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, ApiError> {
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?;

View File

@@ -286,7 +286,7 @@ pub async fn user_icon_edit(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
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 =

View File

@@ -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> {

27
src/util/ext.rs Normal file
View File

@@ -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,
}
}

View File

@@ -1,3 +1,4 @@
pub mod auth;
pub mod ext;
pub mod validate;
pub mod webhook;

View File

@@ -113,5 +113,3 @@ fn game_version_supported(
}
}
}
//todo: fabric/forge validators for 1.8+ respectively