Housekeeping + Fix DB perf issues (#542)

* Housekeeping + fix db perf issues

* run prep
This commit is contained in:
Geometrically
2023-02-22 16:11:14 -07:00
committed by GitHub
parent 9afdc55416
commit 00d09aa01e
21 changed files with 1251 additions and 674 deletions

1239
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,35 +12,35 @@ path = "src/main.rs"
[dependencies] [dependencies]
actix = "0.13.0" actix = "0.13.0"
actix-web = "4.2.1" actix-web = "4.3.0"
actix-rt = "2.7.0" actix-rt = "2.8.0"
actix-multipart = "0.4.0" actix-multipart = "0.5.0"
actix-cors = "0.6.4" actix-cors = "0.6.4"
tokio = { version = "1.21.2", features = ["sync"] } tokio = { version = "1.25.0", features = ["sync"] }
tokio-stream = "0.1.10" tokio-stream = "0.1.11"
futures = "0.3.24" futures = "0.3.26"
futures-timer = "3.0.2" futures-timer = "3.0.2"
async-trait = "0.1.57" async-trait = "0.1.64"
dashmap = "5.4.0" dashmap = "5.4.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
meilisearch-sdk = "0.15.0" meilisearch-sdk = "0.22.0"
rust-s3 = "0.32.3" rust-s3 = "0.32.3"
reqwest = { version = "0.11.12", features = ["json", "multipart"] } reqwest = { version = "0.11.14", features = ["json", "multipart"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_with = "2.0.1" serde_with = "2.2.0"
chrono = { version = "0.4.22", features = ["serde"]} chrono = { version = "0.4.23", features = ["serde"]}
yaserde = "0.8.0" yaserde = "0.8.0"
yaserde_derive = "0.8.0" yaserde_derive = "0.8.0"
xml-rs = "0.8.4" xml-rs = "0.8.4"
rand = "0.8.5" rand = "0.8.5"
bytes = "1.2.1" bytes = "1.4.0"
base64 = "0.20.0" base64 = "0.21.0"
sha1 = { version = "0.6.1", features = ["std"] } sha1 = { version = "0.6.1", features = ["std"] }
sha2 = "0.9.9" sha2 = "0.9.9"
hmac = "0.11.0" hmac = "0.11.0"
@@ -50,25 +50,25 @@ hex = "0.4.3"
url = "2.3.1" url = "2.3.1"
urlencoding = "2.1.2" urlencoding = "2.1.2"
zip = "0.6.3" zip = "0.6.4"
itertools = "0.10.5" itertools = "0.10.5"
validator = { version = "0.16.0", features = ["derive", "phone"] } validator = { version = "0.16.0", features = ["derive", "phone"] }
regex = "1.6.0" regex = "1.7.1"
censor = "0.3.0" censor = "0.3.0"
spdx = { version = "0.9.0", features = ["text"] } spdx = { version = "0.10.0", features = ["text"] }
dotenvy = "0.15.6" dotenvy = "0.15.6"
log = "0.4.17" log = "0.4.17"
env_logger = "0.9.1" env_logger = "0.10.0"
thiserror = "1.0.37" thiserror = "1.0.38"
sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal", "json"] } sqlx = { version = "0.6.2", features = ["runtime-actix-rustls", "postgres", "chrono", "offline", "macros", "migrate", "decimal", "json"] }
rust_decimal = { version = "1.26", features = ["serde-with-float", "serde-with-str"] } rust_decimal = { version = "1.28.1", features = ["serde-with-float", "serde-with-str"] }
sentry = { version = "0.29.2", features = ["profiling"] } sentry = { version = "0.29.3", features = ["profiling"] }
sentry-actix = "0.29.2" sentry-actix = "0.29.3"
image = "0.24.5" image = "0.24.5"
color-thief = "0.2.2" color-thief = "0.2.2"

View File

@@ -12,12 +12,12 @@ services:
POSTGRES_PASSWORD: labrinth POSTGRES_PASSWORD: labrinth
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
meilisearch: meilisearch:
image: getmeili/meilisearch:v0.25.0 image: getmeili/meilisearch:v1.0.1
restart: on-failure restart: on-failure
ports: ports:
- "7700:7700" - "7700:7700"
volumes: volumes:
- meilisearch-data:/data.ms - meilisearch-data:/meili_data
environment: environment:
MEILI_MASTER_KEY: modrinth MEILI_MASTER_KEY: modrinth
volumes: volumes:

View File

@@ -2868,6 +2868,38 @@
}, },
"query": "\n INSERT INTO historical_payouts (user_id, amount, status)\n VALUES ($1, $2, $3)\n " "query": "\n INSERT INTO historical_payouts (user_id, amount, status)\n VALUES ($1, $2, $3)\n "
}, },
"4838777a8ef4371f4f5bb4f4f038bb6d041455f0849a3972a5418d75165ae9c7": {
"describe": {
"columns": [
{
"name": "dependency_id",
"ordinal": 0,
"type_info": "Int8"
},
{
"name": "mod_id",
"ordinal": 1,
"type_info": "Int8"
},
{
"name": "mod_dependency_id",
"ordinal": 2,
"type_info": "Int8"
}
],
"nullable": [
true,
null,
true
],
"parameters": {
"Left": [
"Int8"
]
}
},
"query": "\n SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n LEFT JOIN versions vd ON d.dependency_id = vd.id\n WHERE v.mod_id = $1\n "
},
"49a5d21a1454afc6383b78e468fd0decc75b9163e7286f34ceab22d563a0d3f7": { "49a5d21a1454afc6383b78e468fd0decc75b9163e7286f34ceab22d563a0d3f7": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -2914,6 +2946,43 @@
}, },
"query": "\n UPDATE mods\n SET server_side = $1\n WHERE (id = $2)\n " "query": "\n UPDATE mods\n SET server_side = $1\n WHERE (id = $2)\n "
}, },
"4ad05a5f35600c5dadedfe93e91374ef20ba55c6a9ac6016a01422f2ae8dbb72": {
"describe": {
"columns": [
{
"name": "version_id",
"ordinal": 0,
"type_info": "Int8"
},
{
"name": "mod_id",
"ordinal": 1,
"type_info": "Int8"
},
{
"name": "date_published",
"ordinal": 2,
"type_info": "Timestamptz"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Int8Array",
"VarcharArray",
"VarcharArray",
"Varchar",
"Int8",
"Int8"
]
}
},
"query": "\n SELECT DISTINCT ON(v.date_published, v.id) version_id, v.mod_id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = ANY($1) AND ($4::varchar IS NULL OR v.version_type = $4)\n ORDER BY v.date_published, v.id ASC\n LIMIT $5 OFFSET $6\n "
},
"4b14b5c69f6a0ee4e06e41d7cea425c7c34d6db45895275a2ce8adfa28dc8f72": { "4b14b5c69f6a0ee4e06e41d7cea425c7c34d6db45895275a2ce8adfa28dc8f72": {
"describe": { "describe": {
"columns": [ "columns": [
@@ -3728,6 +3797,27 @@
}, },
"query": "\n UPDATE dependencies\n SET dependency_id = $2\n WHERE id = ANY($1::bigint[])\n " "query": "\n UPDATE dependencies\n SET dependency_id = $2\n WHERE id = ANY($1::bigint[])\n "
}, },
"71abd207410d123f9a50345ddcddee335fea0d0cc6f28762713ee01a36aee8a0": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int8"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Int8Array",
"Int8"
]
}
},
"query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2\n WHERE m.id = ANY($1)\n "
},
"72ad6f4be40d7620a0ec557e3806da41ce95335aeaa910fe35aca2ec7c3f09b6": { "72ad6f4be40d7620a0ec557e3806da41ce95335aeaa910fe35aca2ec7c3f09b6": {
"describe": { "describe": {
"columns": [ "columns": [
@@ -4642,6 +4732,35 @@
}, },
"query": "\n DELETE FROM loaders\n WHERE loader = $1\n " "query": "\n DELETE FROM loaders\n WHERE loader = $1\n "
}, },
"9284d7f22617e0a7daf91540ff31791d0921ec5d4eb4809846dc67567bec1a81": {
"describe": {
"columns": [
{
"name": "hash",
"ordinal": 0,
"type_info": "Bytea"
},
{
"name": "mod_id",
"ordinal": 1,
"type_info": "Int8"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"TextArray",
"ByteaArray",
"Text",
"TextArray"
]
}
},
"query": "\n SELECT h.hash, v.mod_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)\n INNER JOIN mods m on v.mod_id = m.id\n WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)\n "
},
"9348309884811e8b22f33786ae7c0f259f37f3c90e545f00761a641570107160": { "9348309884811e8b22f33786ae7c0f259f37f3c90e545f00761a641570107160": {
"describe": { "describe": {
"columns": [ "columns": [
@@ -4752,6 +4871,33 @@
}, },
"query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)" "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)"
}, },
"980e2ebd1b77baecff5b302b063d8f359ddbdb68452c4c8f2a53dc8d6a2127a4": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int8"
},
{
"name": "team_id",
"ordinal": 1,
"type_info": "Int8"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Int8Array",
"Int8"
]
}
},
"query": "\n SELECT m.id id, m.team_id team_id FROM team_members tm\n INNER JOIN mods m ON m.team_id = tm.team_id\n WHERE tm.team_id = ANY($1) AND tm.user_id = $2\n "
},
"99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": { "99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": {
"describe": { "describe": {
"columns": [], "columns": [],

View File

@@ -117,7 +117,7 @@ pub struct TeamId(pub i64);
#[sqlx(transparent)] #[sqlx(transparent)]
pub struct TeamMemberId(pub i64); pub struct TeamMemberId(pub i64);
#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Deserialize)] #[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Deserialize, Hash)]
#[sqlx(transparent)] #[sqlx(transparent)]
pub struct ProjectId(pub i64); pub struct ProjectId(pub i64);
#[derive(Copy, Clone, Debug, Type)] #[derive(Copy, Clone, Debug, Type)]

View File

@@ -518,6 +518,56 @@ impl Version {
Ok(vec) Ok(vec)
} }
pub async fn get_projects_versions<'a, E>(
project_ids: Vec<ProjectId>,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<String>>,
version_type: Option<VersionType>,
limit: Option<u32>,
offset: Option<u32>,
exec: E,
) -> Result<HashMap<ProjectId, Vec<VersionId>>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let vec = sqlx::query!(
"
SELECT DISTINCT ON(v.date_published, v.id) version_id, v.mod_id, v.date_published FROM versions v
INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id
INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))
INNER JOIN loaders_versions lv ON lv.version_id = v.id
INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))
WHERE v.mod_id = ANY($1) AND ($4::varchar IS NULL OR v.version_type = $4)
ORDER BY v.date_published, v.id ASC
LIMIT $5 OFFSET $6
",
&project_ids.into_iter().map(|x| x.0).collect::<Vec<i64>>(),
&game_versions.unwrap_or_default(),
&loaders.unwrap_or_default(),
version_type.map(|x| x.as_str()),
limit.map(|x| x as i64),
offset.map(|x| x as i64),
)
.fetch_many(exec)
.try_filter_map(|e| async { Ok(e.right().map(|v| (ProjectId(v.mod_id), VersionId(v.version_id)))) })
.try_collect::<Vec<(ProjectId, VersionId)>>()
.await?;
let mut map: HashMap<ProjectId, Vec<VersionId>> = HashMap::new();
for (project_id, version_id) in vec {
if let Some(value) = map.get_mut(&project_id) {
value.push(version_id);
} else {
map.insert(project_id, vec![version_id]);
}
}
Ok(map)
}
pub async fn get<'a, 'b, E>( pub async fn get<'a, 'b, E>(
id: VersionId, id: VersionId,
executor: E, executor: E,

View File

@@ -1,4 +1,5 @@
use crate::file_hosting::FileHostingError; use crate::file_hosting::FileHostingError;
use base64::Engine;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@@ -35,7 +36,10 @@ pub async fn authorize_account(
application_key: &str, application_key: &str,
) -> Result<AuthorizationData, FileHostingError> { ) -> Result<AuthorizationData, FileHostingError> {
let combined_key = format!("{key_id}:{application_key}"); let combined_key = format!("{key_id}:{application_key}");
let formatted_key = format!("Basic {}", base64::encode(combined_key)); let formatted_key = format!(
"Basic {}",
base64::engine::general_purpose::STANDARD.encode(combined_key)
);
let response = reqwest::Client::new() let response = reqwest::Client::new()
.get("https://api.backblazeb2.com/b2api/v2/b2_authorize_account") .get("https://api.backblazeb2.com/b2api/v2/b2_authorize_account")

View File

@@ -1,4 +1,5 @@
use crate::routes::ApiError; use crate::routes::ApiError;
use base64::Engine;
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal; use rust_decimal::Decimal;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -51,7 +52,10 @@ impl PayoutsQueue {
dotenvy::var("PAYPAL_CLIENT_ID")?, dotenvy::var("PAYPAL_CLIENT_ID")?,
dotenvy::var("PAYPAL_CLIENT_SECRET")? dotenvy::var("PAYPAL_CLIENT_SECRET")?
); );
let formatted_key = format!("Basic {}", base64::encode(combined_key)); let formatted_key = format!(
"Basic {}",
base64::engine::general_purpose::STANDARD.encode(combined_key)
);
let mut form = HashMap::new(); let mut form = HashMap::new();
form.insert("grant_type", "client_credentials"); form.insert("grant_type", "client_credentials");

View File

@@ -108,11 +108,13 @@ pub async fn process_payout(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
data: web::Json<PayoutData>, data: web::Json<PayoutData>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let start = data let start: DateTime<Utc> = DateTime::from_utc(
.date data.date
.date() .date_naive()
.and_hms_nano(0, 0, 0, 0) .and_hms_nano_opt(0, 0, 0, 0)
.with_timezone(&Utc); .unwrap_or_default(),
Utc,
);
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;

View File

@@ -267,7 +267,8 @@ pub async fn handle_stripe_webhook(
if let Some(item) = invoice.lines.data.first() { if let Some(item) = invoice.lines.data.first() {
let expires: DateTime<Utc> = DateTime::from_utc( let expires: DateTime<Utc> = DateTime::from_utc(
NaiveDateTime::from_timestamp(item.period.end, 0), NaiveDateTime::from_timestamp_opt(item.period.end, 0)
.unwrap_or_default(),
Utc, Utc,
) + Duration::days(1); ) + Duration::days(1);

View File

@@ -9,15 +9,18 @@ use crate::models::projects::{
use crate::models::teams::Permissions; use crate::models::teams::Permissions;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::search::{search_for_project, SearchConfig, SearchError}; use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::{get_user_from_headers, is_authorized}; use crate::util::auth::{
filter_authorized_projects, get_user_from_headers, is_authorized,
};
use crate::util::routes::read_from_payload; use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures::{StreamExt, TryStreamExt}; use futures::TryStreamExt;
use meilisearch_sdk::indexes::IndexesResults;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use sqlx::{PgPool, Row}; use sqlx::PgPool;
use std::sync::Arc; use std::sync::Arc;
use validator::Validate; use validator::Validate;
@@ -91,16 +94,8 @@ pub async fn projects_get(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let projects: Vec<_> = futures::stream::iter(projects_data) let projects =
.filter_map(|data| async { filter_authorized_projects(projects_data, &user_option, &pool).await?;
if is_authorized(&data.inner, &user_option, &pool).await.ok()? {
Some(Project::from(data))
} else {
None
}
})
.collect()
.await;
Ok(HttpResponse::Ok().json(projects)) Ok(HttpResponse::Ok().json(projects))
} }
@@ -216,26 +211,25 @@ pub async fn dependency_list(
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
//TODO: This query is not checked at compile time! Once SQLX parses this query correctly, please use the query! macro instead let dependencies = sqlx::query!(
let dependencies = sqlx::query(
" "
SELECT d.dependency_id, vd.mod_id, d.mod_dependency_id SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id
FROM versions v FROM versions v
INNER JOIN dependencies d ON d.dependent_id = v.id INNER JOIN dependencies d ON d.dependent_id = v.id
LEFT JOIN versions vd ON d.dependency_id = vd.id LEFT JOIN versions vd ON d.dependency_id = vd.id
WHERE v.mod_id = $1 WHERE v.mod_id = $1
", ",
id as database::models::ProjectId
) )
.bind(id as database::models::ProjectId)
.fetch_many(&**pool) .fetch_many(&**pool)
.try_filter_map(|e| async { .try_filter_map(|e| async {
Ok(e.right().map(|x| { Ok(e.right().map(|x| {
( (
x.get::<Option<i64>, usize>(0) x.dependency_id
.map(database::models::VersionId), .map(database::models::VersionId),
x.get::<Option<i64>, usize>(1) if x.mod_id == Some(0) { None } else { x.mod_id
.map(database::models::ProjectId), .map(database::models::ProjectId) },
x.get::<Option<i64>, usize>(2) x.mod_dependency_id
.map(database::models::ProjectId), .map(database::models::ProjectId),
) )
})) }))
@@ -262,19 +256,20 @@ pub async fn dependency_list(
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let (projects_result, versions_result) = futures::join!( let (projects_result, versions_result) = futures::future::try_join(
database::Project::get_many_full(&project_ids, &**pool,), database::Project::get_many_full(&project_ids, &**pool),
database::Version::get_many_full( database::Version::get_many_full(
dependencies.iter().filter_map(|x| x.0).collect(), dependencies.iter().filter_map(|x| x.0).collect(),
&**pool, &**pool,
) ),
); )
.await?;
let mut projects = projects_result? let mut projects = projects_result
.into_iter() .into_iter()
.map(models::projects::Project::from) .map(models::projects::Project::from)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut versions = versions_result? let mut versions = versions_result
.into_iter() .into_iter()
.map(models::projects::Version::from) .map(models::projects::Version::from)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -2372,9 +2367,9 @@ pub async fn delete_from_index(
let client = let client =
meilisearch_sdk::client::Client::new(&*config.address, &*config.key); meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
let indexes: Vec<meilisearch_sdk::indexes::Index> = let indexes: IndexesResults = client.get_indexes().await?;
client.get_indexes().await?;
for index in indexes { for index in indexes.results {
index.delete_document(id.to_string()).await?; index.delete_document(id.to_string()).await?;
} }

View File

@@ -18,7 +18,8 @@ pub async fn get_stats(
.map(|x| x.to_string()) .map(|x| x.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.fetch_one(&**pool); .fetch_one(&**pool)
.await?;
let versions = sqlx::query!( let versions = sqlx::query!(
" "
@@ -36,7 +37,8 @@ pub async fn get_stats(
.map(|x| x.to_string()) .map(|x| x.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.fetch_one(&**pool); .fetch_one(&**pool)
.await?;
let authors = sqlx::query!( let authors = sqlx::query!(
" "
@@ -50,7 +52,8 @@ pub async fn get_stats(
.map(|x| x.to_string()) .map(|x| x.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.fetch_one(&**pool); .fetch_one(&**pool)
.await?;
let files = sqlx::query!( let files = sqlx::query!(
" "
@@ -67,10 +70,8 @@ pub async fn get_stats(
.map(|x| x.to_string()) .map(|x| x.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.fetch_one(&**pool); .fetch_one(&**pool)
.await?;
let (projects, versions, authors, files) =
futures::future::try_join4(projects, versions, authors, files).await?;
let json = json!({ let json = json!({
"projects": projects.count, "projects": projects.count,

View File

@@ -5,11 +5,10 @@ use serde::Serialize;
use sqlx::PgPool; use sqlx::PgPool;
use crate::database; use crate::database;
use crate::models::projects::{Version, VersionType}; use crate::models::projects::VersionType;
use crate::util::auth::{ use crate::util::auth::{
get_user_from_headers, is_authorized, is_authorized_version, filter_authorized_versions, get_user_from_headers, is_authorized,
}; };
use futures::StreamExt;
use super::ApiError; use super::ApiError;
@@ -48,22 +47,10 @@ pub async fn forge_updates(
let versions = let versions =
database::models::Version::get_many_full(version_ids, &**pool).await?; database::models::Version::get_many_full(version_ids, &**pool).await?;
let mut versions = futures::stream::iter(versions) let mut versions =
.filter_map(|data| async { filter_authorized_versions(versions, &user_option, &pool).await?;
if is_authorized_version(&data.inner, &user_option, &pool)
.await
.ok()?
{
Some(data)
} else {
None
}
})
.collect::<Vec<_>>()
.await;
versions versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
#[derive(Serialize)] #[derive(Serialize)]
struct ForgeUpdates { struct ForgeUpdates {
@@ -81,8 +68,6 @@ pub async fn forge_updates(
}; };
for version in versions { for version in versions {
let version = Version::from(version);
if version.version_type == VersionType::Release { if version.version_type == VersionType::Release {
for game_version in &version.game_versions { for game_version in &version.game_versions {
response response

View File

@@ -7,11 +7,11 @@ use crate::util::auth::get_user_from_headers;
use crate::util::routes::ok_or_not_found; use crate::util::routes::ok_or_not_found;
use crate::{database, models}; use crate::{database, models};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use futures::TryStreamExt;
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::RwLock;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct HashQuery { pub struct HashQuery {
@@ -460,7 +460,7 @@ pub async fn update_files(
let result = sqlx::query!( let result = sqlx::query!(
" "
SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h SELECT h.hash, v.mod_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1) INNER JOIN versions v ON v.id = f.version_id AND v.status != ANY($1)
INNER JOIN mods m on v.mod_id = m.id INNER JOIN mods m on v.mod_id = m.id
@@ -471,49 +471,52 @@ pub async fn update_files(
update_data.algorithm, update_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(), &*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
) )
.fetch_all(&mut *transaction) .fetch_many(&mut *transaction)
.try_filter_map(|e| async {
Ok(e.right().map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id))))
})
.try_collect::<Vec<_>>()
.await?; .await?;
let version_ids: RwLock<HashMap<database::models::VersionId, Vec<u8>>> = let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> =
RwLock::new(HashMap::new()); HashMap::new();
futures::future::try_join_all(result.into_iter().map(|row| async { let updated_versions = database::models::Version::get_projects_versions(
let updated_versions = database::models::Version::get_project_versions( result
database::models::ProjectId(row.project_id), .iter()
Some( .map(|x| x.1)
update_data .collect::<Vec<database::models::ProjectId>>()
.game_versions .clone(),
.clone() Some(
.iter() update_data
.map(|x| x.0.clone()) .game_versions
.collect(), .clone()
), .iter()
Some( .map(|x| x.0.clone())
update_data .collect(),
.loaders ),
.clone() Some(
.iter() update_data
.map(|x| x.0.clone()) .loaders
.collect(), .clone()
), .iter()
None, .map(|x| x.0.clone())
None, .collect(),
None, ),
&**pool, None,
) None,
.await?; None,
&**pool,
if let Some(latest_version) = updated_versions.first() { )
let mut version_ids = version_ids.write().await;
version_ids.insert(*latest_version, row.hash);
}
Ok::<(), ApiError>(())
}))
.await?; .await?;
let version_ids = version_ids.into_inner(); for (hash, id) in result {
if let Some(latest_version) =
updated_versions.get(&id).and_then(|x| x.last())
{
version_ids.insert(*latest_version, hash);
}
}
let versions = database::models::Version::get_many_full( let versions = database::models::Version::get_many_full(
version_ids.keys().copied().collect(), version_ids.keys().copied().collect(),
@@ -533,8 +536,7 @@ pub async fn update_files(
models::projects::Version::from(version), models::projects::Version::from(version),
); );
} else { } else {
let version_id: models::projects::VersionId = let version_id: VersionId = version.inner.id.into();
version.inner.id.into();
return Err(ApiError::Database(DatabaseError::Other(format!( return Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {version_id}" "Could not parse hash for version {version_id}"

View File

@@ -2,16 +2,16 @@ use super::ApiError;
use crate::database; use crate::database;
use crate::models; use crate::models;
use crate::models::projects::{ use crate::models::projects::{
Dependency, FileType, Version, VersionStatus, VersionType, Dependency, FileType, VersionStatus, VersionType,
}; };
use crate::models::teams::Permissions; use crate::models::teams::Permissions;
use crate::util::auth::{ use crate::util::auth::{
get_user_from_headers, is_authorized, is_authorized_version, filter_authorized_versions, get_user_from_headers, is_authorized,
is_authorized_version,
}; };
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures::StreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
use validator::Validate; use validator::Validate;
@@ -70,23 +70,16 @@ pub async fn version_list(
database::models::Version::get_many_full(version_ids, &**pool) database::models::Version::get_many_full(version_ids, &**pool)
.await?; .await?;
let mut response = futures::stream::iter(versions.clone()) let mut response = versions
.filter_map(|data| async { .iter()
if is_authorized_version(&data.inner, &user_option, &pool) .filter(|version| {
.await filters
.ok()? .featured
&& filters .map(|featured| featured == version.inner.featured)
.featured .unwrap_or(true)
.map(|featured| featured == data.inner.featured)
.unwrap_or(true)
{
Some(Version::from(data))
} else {
None
}
}) })
.collect::<Vec<_>>() .cloned()
.await; .collect::<Vec<_>>();
versions.sort_by(|a, b| { versions.sort_by(|a, b| {
b.inner.date_published.cmp(&a.inner.date_published) b.inner.date_published.cmp(&a.inner.date_published)
@@ -97,16 +90,15 @@ pub async fn version_list(
&& !versions.is_empty() && !versions.is_empty()
&& filters.featured.unwrap_or(false) && filters.featured.unwrap_or(false)
{ {
let (loaders, game_versions) = futures::join!( let (loaders, game_versions) = futures::future::try_join(
database::models::categories::Loader::list(&**pool), database::models::categories::Loader::list(&**pool),
database::models::categories::GameVersion::list_filter( database::models::categories::GameVersion::list_filter(
None, None,
Some(true), Some(true),
&**pool &**pool,
) ),
); )
.await?;
let (loaders, game_versions) = (loaders?, game_versions?);
let mut joined_filters = Vec::new(); let mut joined_filters = Vec::new();
for game_version in &game_versions { for game_version in &game_versions {
@@ -122,21 +114,24 @@ pub async fn version_list(
version.game_versions.contains(&filter.0.version) version.game_versions.contains(&filter.0.version)
&& version.loaders.contains(&filter.1.loader) && version.loaders.contains(&filter.1.loader)
}) })
.map(|version| { .map(|version| response.push(version.clone()))
response.push(Version::from(version.clone()))
})
.unwrap_or(()); .unwrap_or(());
}); });
if response.is_empty() { if response.is_empty() {
versions versions
.into_iter() .into_iter()
.for_each(|version| response.push(Version::from(version))); .for_each(|version| response.push(version));
} }
} }
response.sort_by(|a, b| b.date_published.cmp(&a.date_published)); response.sort_by(|a, b| {
response.dedup_by(|a, b| a.id == b.id); b.inner.date_published.cmp(&a.inner.date_published)
});
response.dedup_by(|a, b| a.inner.id == b.inner.id);
let response =
filter_authorized_versions(response, &user_option, &pool).await?;
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(response))
} else { } else {
@@ -190,19 +185,8 @@ pub async fn versions_get(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let versions: Vec<_> = futures::stream::iter(versions_data) let versions =
.filter_map(|data| async { filter_authorized_versions(versions_data, &user_option, &pool).await?;
if is_authorized_version(&data.inner, &user_option, &pool)
.await
.ok()?
{
Some(Version::from(data))
} else {
None
}
})
.collect()
.await;
Ok(HttpResponse::Ok().json(versions)) Ok(HttpResponse::Ok().json(versions))
} }

View File

@@ -4,7 +4,6 @@ use actix_web::http::StatusCode;
use actix_web::HttpResponse; use actix_web::HttpResponse;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use meilisearch_sdk::client::Client; use meilisearch_sdk::client::Client;
use meilisearch_sdk::document::Document;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::min; use std::cmp::min;
@@ -137,22 +136,6 @@ pub struct ResultSearchProject {
pub color: Option<u32>, pub color: Option<u32>,
} }
impl Document for UploadSearchProject {
type UIDType = String;
fn get_uid(&self) -> &Self::UIDType {
&self.project_id
}
}
impl Document for ResultSearchProject {
type UIDType = String;
fn get_uid(&self) -> &Self::UIDType {
&self.project_id
}
}
pub async fn search_for_project( pub async fn search_for_project(
info: &SearchRequest, info: &SearchRequest,
config: &SearchConfig, config: &SearchConfig,
@@ -240,8 +223,8 @@ pub async fn search_for_project(
Ok(SearchResults { Ok(SearchResults {
hits: results.hits.into_iter().map(|r| r.result).collect(), hits: results.hits.into_iter().map(|r| r.result).collect(),
offset: results.offset, offset: results.offset.unwrap_or_default(),
limit: results.limit, limit: results.limit.unwrap_or_default(),
total_hits: results.nb_hits, total_hits: results.estimated_total_hits.unwrap_or_default(),
}) })
} }

View File

@@ -1,4 +1,6 @@
use crate::database; use crate::database;
use crate::database::models::project_item::QueryProject;
use crate::database::models::version_item::QueryVersion;
use crate::database::{models, Project, Version}; use crate::database::{models, Project, Version};
use crate::models::users::{Role, User, UserId, UserPayoutData}; use crate::models::users::{Role, User, UserId, UserPayoutData};
use crate::routes::ApiError; use crate::routes::ApiError;
@@ -161,6 +163,69 @@ pub async fn is_authorized(
Ok(authorized) Ok(authorized)
} }
pub async fn filter_authorized_projects(
projects: Vec<QueryProject>,
user_option: &Option<User>,
pool: &web::Data<PgPool>,
) -> Result<Vec<crate::models::projects::Project>, ApiError> {
let mut return_projects = Vec::new();
let mut check_projects = Vec::new();
for project in projects {
if !project.inner.status.is_hidden()
|| user_option
.as_ref()
.map(|x| x.role.is_mod())
.unwrap_or(false)
{
return_projects.push(project.into());
} else if user_option.is_some() {
check_projects.push(project);
}
}
if !check_projects.is_empty() {
if let Some(user) = user_option {
let user_id: models::ids::UserId = user.id.into();
use futures::TryStreamExt;
sqlx::query!(
"
SELECT m.id id, m.team_id team_id FROM team_members tm
INNER JOIN mods m ON m.team_id = tm.team_id
WHERE tm.team_id = ANY($1) AND tm.user_id = $2
",
&check_projects
.iter()
.map(|x| x.inner.team_id.0)
.collect::<Vec<_>>(),
user_id as database::models::ids::UserId,
)
.fetch_many(&***pool)
.try_for_each(|e| {
if let Some(row) = e.right() {
check_projects.retain(|x| {
let bool = x.inner.id.0 == row.id
&& x.inner.team_id.0 == row.team_id;
if bool {
return_projects.push(x.clone().into());
}
!bool
});
}
futures::future::ready(Ok(()))
})
.await?;
}
}
Ok(return_projects)
}
pub async fn is_authorized_version( pub async fn is_authorized_version(
version_data: &Version, version_data: &Version,
user_option: &Option<User>, user_option: &Option<User>,
@@ -191,3 +256,61 @@ pub async fn is_authorized_version(
Ok(authorized) Ok(authorized)
} }
pub async fn filter_authorized_versions(
versions: Vec<QueryVersion>,
user_option: &Option<User>,
pool: &web::Data<PgPool>,
) -> Result<Vec<crate::models::projects::Version>, ApiError> {
let mut return_versions = Vec::new();
let mut check_versions = Vec::new();
for version in versions {
if !version.inner.status.is_hidden()
|| user_option
.as_ref()
.map(|x| x.role.is_mod())
.unwrap_or(false)
{
return_versions.push(version.into());
} else if user_option.is_some() {
check_versions.push(version);
}
}
if !check_versions.is_empty() {
if let Some(user) = user_option {
let user_id: models::ids::UserId = user.id.into();
use futures::TryStreamExt;
sqlx::query!(
"
SELECT m.id FROM mods m
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2
WHERE m.id = ANY($1)
",
&check_versions.iter().map(|x| x.inner.project_id.0).collect::<Vec<_>>(),
user_id as database::models::ids::UserId,
)
.fetch_many(&***pool)
.try_for_each(|e| {
if let Some(row) = e.right() {
check_versions.retain(|x| {
let bool = x.inner.project_id.0 == row.id;
if bool {
return_versions.push(x.clone().into());
}
!bool
});
}
futures::future::ready(Ok(()))
}).await?;
}
}
Ok(return_versions)
}

View File

@@ -23,7 +23,7 @@ impl super::Validator for FabricValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions { fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Time since release of 18w49a, the first fabric version // Time since release of 18w49a, the first fabric version
SupportedGameVersions::PastDate(DateTime::from_utc( SupportedGameVersions::PastDate(DateTime::from_utc(
NaiveDateTime::from_timestamp(1543969469, 0), NaiveDateTime::from_timestamp_opt(1543969469, 0).unwrap(),
Utc, Utc,
)) ))
} }

View File

@@ -23,7 +23,7 @@ impl super::Validator for ForgeValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions { fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Time since release of 1.13, the first forge version which uses the new TOML system // Time since release of 1.13, the first forge version which uses the new TOML system
SupportedGameVersions::PastDate(DateTime::<Utc>::from_utc( SupportedGameVersions::PastDate(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(1540122067, 0), NaiveDateTime::from_timestamp_opt(1540122067, 0).unwrap(),
Utc, Utc,
)) ))
} }
@@ -69,11 +69,11 @@ impl super::Validator for LegacyForgeValidator {
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods // Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
SupportedGameVersions::Range( SupportedGameVersions::Range(
DateTime::from_utc( DateTime::from_utc(
NaiveDateTime::from_timestamp(1366818300, 0), NaiveDateTime::from_timestamp_opt(1366818300, 0).unwrap(),
Utc, Utc,
), ),
DateTime::from_utc( DateTime::from_utc(
NaiveDateTime::from_timestamp(1505810340, 0), NaiveDateTime::from_timestamp_opt(1505810340, 0).unwrap(),
Utc, Utc,
), ),
) )

View File

@@ -22,7 +22,7 @@ impl super::Validator for QuiltValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions { fn get_supported_game_versions(&self) -> SupportedGameVersions {
SupportedGameVersions::PastDate(DateTime::from_utc( SupportedGameVersions::PastDate(DateTime::from_utc(
NaiveDateTime::from_timestamp(1646070100, 0), NaiveDateTime::from_timestamp_opt(1646070100, 0).unwrap(),
Utc, Utc,
)) ))
} }

View File

@@ -23,7 +23,7 @@ impl super::Validator for PackValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions { fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Time since release of 13w24a which replaced texture packs with resource packs // Time since release of 13w24a which replaced texture packs with resource packs
SupportedGameVersions::PastDate(DateTime::from_utc( SupportedGameVersions::PastDate(DateTime::from_utc(
NaiveDateTime::from_timestamp(1371137542, 0), NaiveDateTime::from_timestamp_opt(1371137542, 0).unwrap(),
Utc, Utc,
)) ))
} }
@@ -61,11 +61,11 @@ impl super::Validator for TexturePackValidator {
// a1.2.2a to 13w23b // a1.2.2a to 13w23b
SupportedGameVersions::Range( SupportedGameVersions::Range(
DateTime::from_utc( DateTime::from_utc(
NaiveDateTime::from_timestamp(1289339999, 0), NaiveDateTime::from_timestamp_opt(1289339999, 0).unwrap(),
Utc, Utc,
), ),
DateTime::from_utc( DateTime::from_utc(
NaiveDateTime::from_timestamp(1370651522, 0), NaiveDateTime::from_timestamp_opt(1370651522, 0).unwrap(),
Utc, Utc,
), ),
) )