From c14265e626865ef051cae806d39fe02a0dba8c42 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 12 Mar 2026 22:47:51 +0200 Subject: [PATCH 01/69] feat(submissions): initial impl --- ...ate_mod_version_submissions_table.down.sql | 4 + ...reate_mod_version_submissions_table.up.sql | 25 + src/database/repository/developers.rs | 26 ++ src/database/repository/mod.rs | 1 + .../repository/mod_version_submissions.rs | 174 +++++++ src/endpoints/mod.rs | 1 + src/endpoints/mod_version_submissions.rs | 429 ++++++++++++++++++ src/endpoints/mod_versions.rs | 7 +- src/endpoints/mods.rs | 7 +- src/main.rs | 6 + src/openapi.rs | 12 + src/types/models/mod.rs | 7 +- src/types/models/mod_version_submission.rs | 81 ++++ 13 files changed, 775 insertions(+), 5 deletions(-) create mode 100644 migrations/20260310175835_create_mod_version_submissions_table.down.sql create mode 100644 migrations/20260310175835_create_mod_version_submissions_table.up.sql create mode 100644 src/database/repository/mod_version_submissions.rs create mode 100644 src/endpoints/mod_version_submissions.rs create mode 100644 src/types/models/mod_version_submission.rs diff --git a/migrations/20260310175835_create_mod_version_submissions_table.down.sql b/migrations/20260310175835_create_mod_version_submissions_table.down.sql new file mode 100644 index 00000000..81adb2d9 --- /dev/null +++ b/migrations/20260310175835_create_mod_version_submissions_table.down.sql @@ -0,0 +1,4 @@ +-- Add down migration script here + +DROP TABLE IF EXISTS mod_version_submission_comments; +DROP TABLE IF EXISTS mod_version_submissions; \ No newline at end of file diff --git a/migrations/20260310175835_create_mod_version_submissions_table.up.sql b/migrations/20260310175835_create_mod_version_submissions_table.up.sql new file mode 100644 index 00000000..6e442084 --- /dev/null +++ b/migrations/20260310175835_create_mod_version_submissions_table.up.sql @@ -0,0 +1,25 @@ +-- Add up migration script here + +CREATE TABLE mod_version_submissions ( + mod_version_id INT NOT NULL PRIMARY KEY, + locked BOOLEAN NOT NULL DEFAULT FALSE, + locked_by INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + FOREIGN KEY (mod_version_id) REFERENCES mod_versions(id) ON DELETE CASCADE, + FOREIGN KEY (locked_by) REFERENCES developers(id) ON DELETE SET NULL +); + +CREATE TABLE mod_version_submission_comments ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + submission_id INT NOT NULL, + author_id INT NOT NULL, + comment TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ, + FOREIGN KEY (submission_id) REFERENCES mod_version_submissions(mod_version_id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES developers(id) ON DELETE RESTRICT +); + +CREATE INDEX idx_submission_comments_submission_id + ON mod_version_submission_comments(submission_id); diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 18eae63b..c8037e0d 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -139,6 +139,32 @@ pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result Result, DatabaseError> { + if ids.is_empty() { + return Ok(vec![]); + } + sqlx::query_as!( + Developer, + "SELECT + id, + username, + display_name, + verified, + admin, + github_user_id as github_id + FROM developers + WHERE id = ANY($1)", + ids + ) + .fetch_all(&mut *conn) + .await + .inspect_err(|e| log::error!("Failed to fetch developers by id: {e}")) + .map_err(|e| e.into()) +} + pub async fn get_one_by_username( username: &str, conn: &mut PgConnection, diff --git a/src/database/repository/mod.rs b/src/database/repository/mod.rs index 1073d211..72539227 100644 --- a/src/database/repository/mod.rs +++ b/src/database/repository/mod.rs @@ -10,6 +10,7 @@ pub mod mod_gd_versions; pub mod mod_links; pub mod mod_tags; pub mod mod_version_statuses; +pub mod mod_version_submissions; pub mod mod_versions; pub mod mods; pub mod refresh_tokens; diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs new file mode 100644 index 00000000..0d7bf3eb --- /dev/null +++ b/src/database/repository/mod_version_submissions.rs @@ -0,0 +1,174 @@ +use crate::database::DatabaseError; +use crate::types::models::mod_version_submission::{ModVersionSubmissionCommentRow, ModVersionSubmissionRow}; +use sqlx::{Error, PgConnection}; + +pub async fn get_for_mod_version( + id: i32, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!( + ModVersionSubmissionRow, + "SELECT + mod_version_id, locked, locked_by, + created_at, updated_at + FROM mod_version_submissions + WHERE mod_version_id = $1", + id + ) + .fetch_optional(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::get_for_mod_versions failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn create( + mod_version_id: i32, + conn: &mut PgConnection, +) -> Result { + sqlx::query_as!( + ModVersionSubmissionRow, + "INSERT INTO mod_version_submissions (mod_version_id) + VALUES ($1) + RETURNING mod_version_id, locked, locked_by, created_at, updated_at", + mod_version_id + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::create failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn set_locked( + mod_version_id: i32, + locked: bool, + locked_by: Option, + conn: &mut PgConnection, +) -> Result { + sqlx::query_as!( + ModVersionSubmissionRow, + "UPDATE mod_version_submissions + SET locked = $1, locked_by = $2, updated_at = NOW() + WHERE mod_version_id = $3 + RETURNING mod_version_id, locked, locked_by, created_at, updated_at", + locked, + locked_by, + mod_version_id + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::set_locked failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn get_paginated_comments_for_submission( + id: i32, + page: i64, + per_page: i64, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!( + ModVersionSubmissionCommentRow, + "SELECT + id, submission_id, comment, author_id, + created_at, updated_at + FROM mod_version_submission_comments + WHERE submission_id = $1 + ORDER BY id DESC + LIMIT $2 OFFSET $3", + id, + per_page, + (page - 1) * per_page + ) + .fetch_all(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::get_paginated_items_for_submission failed: {e}")) + .map_err(|e: Error| e.into()) +} + +pub async fn count_comments_for_submission( + id: i32, + conn: &mut PgConnection, +) -> Result { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM mod_version_submission_comments WHERE submission_id = $1", + id + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::count_comments_for_submission failed: {e}")) + .map(|c| c.unwrap_or(0)) + .map_err(|e| e.into()) +} + +pub async fn create_comment( + submission_id: i32, + author_id: i32, + comment: &str, + conn: &mut PgConnection, +) -> Result { + sqlx::query_as!( + ModVersionSubmissionCommentRow, + "INSERT INTO mod_version_submission_comments (submission_id, author_id, comment) + VALUES ($1, $2, $3) + RETURNING id, submission_id, comment, author_id, created_at, updated_at", + submission_id, + author_id, + comment + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::create_comment failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn get_comment( + comment_id: i64, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!( + ModVersionSubmissionCommentRow, + "SELECT id, submission_id, comment, author_id, created_at, updated_at + FROM mod_version_submission_comments + WHERE id = $1", + comment_id + ) + .fetch_optional(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::get_comment failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn update_comment( + comment_id: i64, + new_text: &str, + conn: &mut PgConnection, +) -> Result { + sqlx::query_as!( + ModVersionSubmissionCommentRow, + "UPDATE mod_version_submission_comments + SET comment = $1, updated_at = NOW() + WHERE id = $2 + RETURNING id, submission_id, comment, author_id, created_at, updated_at", + new_text, + comment_id + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::update_comment failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn delete_comment( + comment_id: i64, + conn: &mut PgConnection, +) -> Result { + let result = sqlx::query!( + "DELETE FROM mod_version_submission_comments WHERE id = $1", + comment_id + ) + .execute(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::delete_comment failed: {e}"))?; + Ok(result.rows_affected() > 0) +} + diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index b7a00e10..ca921420 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -13,6 +13,7 @@ pub mod mods; pub mod stats; pub mod tags; pub mod deprecations; +pub mod mod_version_submissions; #[derive(thiserror::Error, Debug)] pub enum ApiError { diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs new file mode 100644 index 00000000..5d39d5b0 --- /dev/null +++ b/src/endpoints/mod_version_submissions.rs @@ -0,0 +1,429 @@ +use std::collections::HashMap; +use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; +use serde::Deserialize; +use sqlx::PgConnection; +use utoipa::IntoParams; + +use super::ApiError; +use crate::config::AppData; +use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; +use crate::extractors::auth::Auth; +use crate::types::api::{ApiResponse, PaginatedData}; +use crate::types::models::mod_version_submission::{ + CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionComment, UpdateCommentPayload, + UpdateSubmissionPayload, +}; + +#[derive(Deserialize, IntoParams)] +struct SubmissionPath { + id: String, + version: String, +} + +#[derive(Deserialize, IntoParams)] +struct CommentPath { + id: String, + version: String, + comment_id: i64, +} + +#[derive(Deserialize, IntoParams)] +struct CommentsQuery { + page: Option, + per_page: Option, +} + +/// Resolve a mod-version's numeric id from its string version tag, and +/// return both it and the verified-mod id. +async fn resolve_version_id( + mod_id: &str, + version: &str, + pool: &mut PgConnection, +) -> Result { + let ver = mod_versions::get_by_version_str(mod_id, version, pool) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Version {} not found", version)))?; + Ok(ver.id) +} + +/// Get the submission for a mod version +#[utoipa::path( + get, + path = "/v1/mods/{id}/versions/{version}/submission", + tag = "mod_version_submissions", + params(SubmissionPath), + responses( + (status = 200, description = "Submission details", body = inline(ApiResponse)), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Mod, version, or submission not found"), + ), + security(("bearer_token" = [])) +)] +#[get("v1/mods/{id}/versions/{version}/submission")] +pub async fn get_submission( + path: web::Path, + data: web::Data, + auth: Auth, +) -> Result { + auth.developer()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + let row = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + let locked_by = match row.locked_by { + Some(dev_id) => Some( + developers::get_one(dev_id, &mut pool) + .await? + .ok_or_else(|| ApiError::InternalError("Locked-by developer not found".into()))?, + ), + None => None, + }; + + Ok(web::Json(ApiResponse { + error: "".into(), + payload: row.into_submission(locked_by), + })) +} + +/// Update (lock / unlock) a submission (admin only) +#[utoipa::path( + put, + path = "/v1/mods/{id}/versions/{version}/submission", + tag = "mod_version_submissions", + params(SubmissionPath), + request_body = UpdateSubmissionPayload, + responses( + (status = 200, description = "Submission updated", body = inline(ApiResponse)), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Mod, version, or submission not found"), + ), + security(("bearer_token" = [])) +)] +#[put("v1/mods/{id}/versions/{version}/submission")] +pub async fn update_submission( + path: web::Path, + data: web::Data, + payload: web::Json, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + auth.check_admin()?; + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + let locked_by_id = if payload.locked { + Some(dev.id) + } else { + None + }; + + let row = + mod_version_submissions::set_locked(version_id, payload.locked, locked_by_id, &mut pool) + .await?; + + let locked_by = if payload.locked { + Some(dev.clone()) + } else { + None + }; + + Ok(web::Json(ApiResponse { + error: "".into(), + payload: row.into_submission(locked_by), + })) +} + +/// List comments for a mod version submission +#[utoipa::path( + get, + path = "/v1/mods/{id}/versions/{version}/submission/comments", + tag = "mod_version_submissions", + params(SubmissionPath, CommentsQuery), + responses( + (status = 200, description = "List of comments", body = inline(ApiResponse>)), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Mod, version, or submission not found"), + ), + security(("bearer_token" = [])) +)] +#[get("v1/mods/{id}/versions/{version}/submission/comments")] +pub async fn get_comments( + path: web::Path, + data: web::Data, + query: web::Query, + auth: Auth, +) -> Result { + auth.developer()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + let page = query.page.unwrap_or(1).max(1); + let per_page = query.per_page.unwrap_or(20).clamp(1, 100); + + let count = + mod_version_submissions::count_comments_for_submission(version_id, &mut pool).await?; + let rows = mod_version_submissions::get_paginated_comments_for_submission( + version_id, page, per_page, &mut pool, + ) + .await?; + + let author_ids: Vec = { + let mut ids: Vec = rows.iter().map(|r| r.author_id).collect(); + ids.sort_unstable(); + ids.dedup(); + ids + }; + + let mut authors_map = developers::get_many_by_id(&author_ids, &mut pool) + .await? + .into_iter() + .map(|dev| (dev.id, dev)) + .collect::>(); + + let comments = rows + .into_iter() + .map(|row| { + let author = authors_map + .get(&row.author_id) + .cloned() + .ok_or_else(|| ApiError::InternalError("Author not found".into()))?; + Ok(row.into_comment(author)) + }) + .collect::, ApiError>>()?; + + Ok(web::Json(ApiResponse { + error: "".into(), + payload: PaginatedData { + data: comments, + count, + }, + })) +} + +/// Add a comment to a mod version submission +#[utoipa::path( + post, + path = "/v1/mods/{id}/versions/{version}/submission/comments", + tag = "mod_version_submissions", + params(SubmissionPath), + request_body = CreateCommentPayload, + responses( + (status = 201, description = "Comment created", body = inline(ApiResponse)), + (status = 400, description = "Bad request - locked submission or empty comment"), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Mod, version, or submission not found"), + ), + security(("bearer_token" = [])) +)] +#[post("v1/mods/{id}/versions/{version}/submission/comments")] +pub async fn create_comment( + path: web::Path, + data: web::Data, + payload: web::Json, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + + let comment_text = payload.comment.trim().to_string(); + if comment_text.is_empty() { + return Err(ApiError::BadRequest("Comment must not be empty".into())); + } + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + if submission.locked { + return Err(ApiError::BadRequest( + "Submission is locked; no new comments allowed".into(), + )); + } + + // Only the mod developers (or admins) may comment + if !dev.admin && !developers::has_access_to_mod(dev.id, &path.id, &mut pool).await? { + return Err(ApiError::Authorization); + } + + let row = mod_version_submissions::create_comment(version_id, dev.id, &comment_text, &mut pool) + .await?; + + Ok(HttpResponse::Created().json(ApiResponse { + error: "".into(), + payload: row.into_comment(dev), + })) +} + +/// Update a comment on a mod version submission +#[utoipa::path( + put, + path = "/v1/mods/{id}/versions/{version}/submission/comments/{comment_id}", + tag = "mod_version_submissions", + params(CommentPath), + request_body = UpdateCommentPayload, + responses( + (status = 200, description = "Comment updated", body = inline(ApiResponse)), + (status = 400, description = "Bad request – locked submission or empty comment"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden – may only edit own comments (admins can edit any)"), + (status = 404, description = "Mod, version, submission, or comment not found"), + ), + security(("bearer_token" = [])) +)] +#[put("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] +pub async fn update_comment( + path: web::Path, + data: web::Data, + payload: web::Json, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + + let comment_text = payload.comment.trim().to_string(); + if comment_text.is_empty() { + return Err(ApiError::BadRequest("Comment must not be empty".into())); + } + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + if submission.locked { + return Err(ApiError::BadRequest( + "Submission is locked; comments cannot be edited".into(), + )); + } + + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; + + if comment_row.submission_id != version_id { + return Err(ApiError::NotFound(format!( + "Comment {} does not belong to this submission", + path.comment_id + ))); + } + + if !dev.admin && comment_row.author_id != dev.id { + return Err(ApiError::Authorization); + } + + let updated_row = + mod_version_submissions::update_comment(path.comment_id, &comment_text, &mut pool).await?; + + let author = developers::get_one(updated_row.author_id, &mut pool) + .await? + .ok_or_else(|| ApiError::InternalError("Author not found".into()))?; + + Ok(web::Json(ApiResponse { + error: "".into(), + payload: updated_row.into_comment(author), + })) +} + +// ── DELETE comment ──────────────────────────────────────────────────────────── + +/// Delete a comment on a mod version submission +#[utoipa::path( + delete, + path = "/v1/mods/{id}/versions/{version}/submission/comments/{comment_id}", + tag = "mod_version_submissions", + params(CommentPath), + responses( + (status = 204, description = "Comment deleted"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden – may only delete own comments (admins can delete any)"), + (status = 404, description = "Mod, version, submission, or comment not found"), + ), + security(("bearer_token" = [])) +)] +#[delete("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] +pub async fn delete_comment( + path: web::Path, + data: web::Data, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + if submission.locked && !dev.admin { + return Err(ApiError::BadRequest( + "Submission is locked; comments cannot be deleted".into(), + )); + } + + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; + + if comment_row.submission_id != version_id { + return Err(ApiError::NotFound(format!( + "Comment {} does not belong to this submission", + path.comment_id + ))); + } + + if !dev.admin && comment_row.author_id != dev.id { + return Err(ApiError::Authorization); + } + + mod_version_submissions::delete_comment(path.comment_id, &mut pool).await?; + + Ok(HttpResponse::NoContent()) +} diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 487fcc4e..285d2f23 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -8,7 +8,7 @@ use utoipa::{ToSchema, IntoParams}; use crate::config::AppData; use crate::database::repository::{ dependencies, developers, incompatibilities, mod_downloads, mod_gd_versions, mod_links, - mod_tags, mod_versions, mods, + mod_tags, mod_version_submissions, mod_versions, mods, }; use crate::endpoints::ApiError; use crate::events::mod_created::{ @@ -437,6 +437,11 @@ pub async fn create_version( mods::update_with_json_moved(the_mod, json, &mut tx).await?; } + + if !make_accepted { + mod_version_submissions::create(version.id, &mut tx).await?; + } + tx.commit().await?; if make_accepted { diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index 9bc5c934..7050c4cb 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -6,7 +6,7 @@ use crate::database::repository::mod_links; use crate::database::repository::mod_tags; use crate::database::repository::mod_versions; use crate::database::repository::mods; -use crate::database::repository::{dependencies, deprecations}; +use crate::database::repository::{dependencies, deprecations, mod_version_submissions}; use crate::endpoints::ApiError; use crate::events::mod_feature::ModFeaturedEvent; use crate::extractors::auth::Auth; @@ -271,6 +271,11 @@ pub async fn create( the_mod.developers = developers::get_all_for_mod(&the_mod.id, &mut tx).await?; the_mod.versions.insert(0, version); + + // First version is always pending, so always open a submission for review + let first_version = the_mod.versions.first().unwrap(); + mod_version_submissions::create(first_version.id, &mut tx).await?; + tx.commit().await?; for i in &mut the_mod.versions { diff --git a/src/main.rs b/src/main.rs index 32ac1f7b..8757ca3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,12 @@ async fn main() -> anyhow::Result<()> { .service(endpoints::mod_versions::download_version) .service(endpoints::mod_versions::create_version) .service(endpoints::mod_versions::update_version) + .service(endpoints::mod_version_submissions::get_submission) + .service(endpoints::mod_version_submissions::update_submission) + .service(endpoints::mod_version_submissions::get_comments) + .service(endpoints::mod_version_submissions::create_comment) + .service(endpoints::mod_version_submissions::update_comment) + .service(endpoints::mod_version_submissions::delete_comment) .service(endpoints::deprecations::index) .service(endpoints::deprecations::store) .service(endpoints::deprecations::update) diff --git a/src/openapi.rs b/src/openapi.rs index 6db8861a..77dd0903 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -16,6 +16,12 @@ use crate::{endpoints, types}; endpoints::mod_versions::download_version, endpoints::mod_versions::create_version, endpoints::mod_versions::update_version, + endpoints::mod_version_submissions::get_submission, + endpoints::mod_version_submissions::update_submission, + endpoints::mod_version_submissions::get_comments, + endpoints::mod_version_submissions::create_comment, + endpoints::mod_version_submissions::update_comment, + endpoints::mod_version_submissions::delete_comment, endpoints::deprecations::index, endpoints::deprecations::store, endpoints::deprecations::update, @@ -72,6 +78,11 @@ use crate::{endpoints, types}; types::models::mod_link::ModLinks, types::models::loader_version::LoaderVersion, types::models::gd_version_alias::GDVersionAlias, + types::models::mod_version_submission::ModVersionSubmission, + types::models::mod_version_submission::ModVersionSubmissionComment, + types::models::mod_version_submission::UpdateSubmissionPayload, + types::models::mod_version_submission::CreateCommentPayload, + types::models::mod_version_submission::UpdateCommentPayload, endpoints::mods::IndexSortType, endpoints::developers::SimpleDevMod, endpoints::developers::SimpleDevModVersion, @@ -80,6 +91,7 @@ use crate::{endpoints, types}; tags( (name = "mods", description = "Mod management endpoints"), (name = "mod_versions", description = "Mod version management endpoints"), + (name = "mod_version_submissions", description = "Mod version submission and review endpoints"), (name = "deprecations", description = "Mod deprecation management endpoints"), (name = "developers", description = "Developer management endpoints"), (name = "tags", description = "Tag management endpoints"), diff --git a/src/types/models/mod.rs b/src/types/models/mod.rs index f9719dc9..ac1abd90 100644 --- a/src/types/models/mod.rs +++ b/src/types/models/mod.rs @@ -1,14 +1,15 @@ pub mod dependency; +pub mod deprecations; pub mod developer; +pub mod gd_version_alias; pub mod github_login_attempt; pub mod incompatibility; +pub mod loader_version; pub mod mod_entity; pub mod mod_gd_version; pub mod mod_link; pub mod mod_version; pub mod mod_version_status; +pub mod mod_version_submission; pub mod stats; pub mod tag; -pub mod loader_version; -pub mod gd_version_alias; -pub mod deprecations; diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs new file mode 100644 index 00000000..4cd224fc --- /dev/null +++ b/src/types/models/mod_version_submission.rs @@ -0,0 +1,81 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use crate::types::models::developer::Developer; + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct ModVersionSubmission { + pub mod_version_id: i32, + pub locked: bool, + pub locked_by: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +pub struct ModVersionSubmissionRow { + pub mod_version_id: i32, + pub locked: bool, + pub locked_by: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl ModVersionSubmissionRow { + pub fn into_submission(self, locked_by: Option) -> ModVersionSubmission { + ModVersionSubmission { + mod_version_id: self.mod_version_id, + locked: self.locked, + locked_by, + created_at: self.created_at, + updated_at: self.updated_at, + } + } +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct ModVersionSubmissionComment { + pub id: i64, + pub submission_id: i32, + pub comment: String, + pub author: Developer, + pub created_at: DateTime, + pub updated_at: Option>, +} + +pub struct ModVersionSubmissionCommentRow { + pub id: i64, + pub submission_id: i32, + pub comment: String, + pub author_id: i32, + pub created_at: DateTime, + pub updated_at: Option>, +} + +impl ModVersionSubmissionCommentRow { + pub fn into_comment(self, author: Developer) -> ModVersionSubmissionComment { + ModVersionSubmissionComment { + id: self.id, + submission_id: self.submission_id, + comment: self.comment, + author, + created_at: self.created_at, + updated_at: self.updated_at, + } + } +} + +#[derive(Deserialize, ToSchema)] +pub struct UpdateSubmissionPayload { + pub locked: bool, +} + +#[derive(Deserialize, ToSchema)] +pub struct CreateCommentPayload { + pub comment: String, +} + +#[derive(Deserialize, ToSchema)] +pub struct UpdateCommentPayload { + pub comment: String, +} + From a2746209d22c7aa4708aa8db1a72a82d7c53d667 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 12 Mar 2026 22:52:42 +0200 Subject: [PATCH 02/69] chore: update deps --- Cargo.lock | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 +- 2 files changed, 159 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a95c59f..e5f61764 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" dependencies = [ "actix-utils", "actix-web", - "derive_more", + "derive_more 2.1.1", "futures-util", "log", "once_cell", @@ -49,7 +49,7 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more", + "derive_more 2.1.1", "encoding_rs", "flate2", "foldhash", @@ -83,6 +83,44 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-multipart" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "derive_more 0.99.20", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "rand 0.8.5", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "actix-router" version = "0.5.3" @@ -164,7 +202,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more", + "derive_more 2.1.1", "encoding_rs", "foldhash", "futures-core", @@ -777,6 +815,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.10.0" @@ -914,6 +958,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deflate64" version = "0.1.10" @@ -951,6 +1030,19 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -966,7 +1058,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -1116,6 +1208,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fax" version = "0.2.6" @@ -1315,6 +1413,7 @@ name = "geode-index" version = "0.51.3" dependencies = [ "actix-cors", + "actix-multipart", "actix-web", "anyhow", "chrono", @@ -1701,6 +1800,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1958,6 +2063,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -2014,7 +2125,7 @@ dependencies = [ "anyhow", "arc-swap", "chrono", - "derive_more", + "derive_more 2.1.1", "fnv", "humantime", "libc", @@ -2320,6 +2431,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "paste" version = "1.0.15" @@ -2877,6 +2994,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.36" @@ -3070,6 +3200,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3489,6 +3628,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 8207d940..4e1dedff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ version = "0.51.3" edition = "2024" [dependencies] -image = { version = "0.25", features = ["png"] } +image = { version = "0.25", features = ["png", "jpeg", "webp"] } +actix-multipart = "0.7" actix-web = "4.10" anyhow = "1.0" dotenvy = "0.15" From 9d90bbcf9fd6ed7f75b32f58b199b14fb9f3afe7 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 12 Mar 2026 23:24:15 +0200 Subject: [PATCH 03/69] feat: add attachments --- .env.example | 6 +- Cargo.lock | 1 + Cargo.toml | 1 + ...te_submission_comment_attachments.down.sql | 2 + ...eate_submission_comment_attachments.up.sql | 16 + src/config.rs | 7 + .../repository/mod_version_submissions.rs | 104 +++++- src/endpoints/mod_version_submissions.rs | 302 +++++++++++++++++- src/main.rs | 7 + src/openapi.rs | 4 + src/types/models/mod_version_submission.rs | 29 ++ 11 files changed, 465 insertions(+), 14 deletions(-) create mode 100644 migrations/20260312205413_create_submission_comment_attachments.down.sql create mode 100644 migrations/20260312205413_create_submission_comment_attachments.up.sql diff --git a/.env.example b/.env.example index 2c4cf7b3..a56aa470 100644 --- a/.env.example +++ b/.env.example @@ -20,4 +20,8 @@ DISCORD_WEBHOOK_URL= MAX_MOD_FILESIZE_MB=250 # Globally disables download counting, in the event of abuse -DISABLE_DOWNLOAD_COUNTS=0 \ No newline at end of file +DISABLE_DOWNLOAD_COUNTS=0 + +# Path where uploaded files are stored on disk (default: ./storage) +# The reverse proxy must serve {STORAGE_PATH}/submission_attachments/ at /storage/submission-attachments/ +STORAGE_PATH=./storage \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e5f61764..dc3bf5c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,6 +1416,7 @@ dependencies = [ "actix-multipart", "actix-web", "anyhow", + "bytes", "chrono", "clap", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 4e1dedff..e9e8ccaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,3 +44,4 @@ thiserror = "2.0.12" moka = { version = "0.12.13", features = ["future"] } utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "uuid"] } utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web"] } +bytes = "1.11" diff --git a/migrations/20260312205413_create_submission_comment_attachments.down.sql b/migrations/20260312205413_create_submission_comment_attachments.down.sql new file mode 100644 index 00000000..42513aa0 --- /dev/null +++ b/migrations/20260312205413_create_submission_comment_attachments.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE IF EXISTS mod_version_submission_comment_attachments; diff --git a/migrations/20260312205413_create_submission_comment_attachments.up.sql b/migrations/20260312205413_create_submission_comment_attachments.up.sql new file mode 100644 index 00000000..6b6ea1d3 --- /dev/null +++ b/migrations/20260312205413_create_submission_comment_attachments.up.sql @@ -0,0 +1,16 @@ +-- Add up migration script here +CREATE TABLE mod_version_submission_comment_attachments +( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + comment_id BIGINT NOT NULL, + filename TEXT NOT NULL, -- SHA-256 hex of WebP content; file on disk: {STORAGE_PATH}/submission_attachments/{filename} + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + FOREIGN KEY (comment_id) REFERENCES mod_version_submission_comments (id) ON DELETE CASCADE +); + +CREATE INDEX idx_submission_attachments_comment_id + ON mod_version_submission_comment_attachments (comment_id); + +-- For fast COUNT lookups on delete (deduplication check) +CREATE INDEX idx_submission_attachments_filename + ON mod_version_submission_comment_attachments (filename); diff --git a/src/config.rs b/src/config.rs index 3353e71f..2213af9c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,7 @@ pub struct AppData { max_download_mb: u32, port: u16, debug: bool, + storage_path: String, mods_cache: Cache>>, } @@ -53,6 +54,7 @@ pub async fn build_config() -> anyhow::Result { .unwrap_or("250".to_string()) .parse::() .unwrap_or(250); + let storage_path = dotenvy::var("STORAGE_PATH").unwrap_or("./storage".to_string()); let mods_cache = Cache::builder() .max_capacity(128) .time_to_idle(Duration::from_mins(5)) @@ -72,6 +74,7 @@ pub async fn build_config() -> anyhow::Result { max_download_mb, port, debug, + storage_path, mods_cache, }) } @@ -123,6 +126,10 @@ impl AppData { self.debug } + pub fn storage_path(&self) -> &str { + &self.storage_path + } + pub fn mods_cache(&self) -> &Cache>> { &self.mods_cache } diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index 0d7bf3eb..dc30f731 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -1,5 +1,7 @@ use crate::database::DatabaseError; -use crate::types::models::mod_version_submission::{ModVersionSubmissionCommentRow, ModVersionSubmissionRow}; +use crate::types::models::mod_version_submission::{ + ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionRow, +}; use sqlx::{Error, PgConnection}; pub async fn get_for_mod_version( @@ -82,7 +84,7 @@ pub async fn get_paginated_comments_for_submission( .fetch_all(conn) .await .inspect_err(|e| log::error!("mod_version_submissions::get_paginated_items_for_submission failed: {e}")) - .map_err(|e: Error| e.into()) + .map_err(|e| e.into()) } pub async fn count_comments_for_submission( @@ -172,3 +174,101 @@ pub async fn delete_comment( Ok(result.rows_affected() > 0) } +pub async fn count_attachments_for_comment( + comment_id: i64, + conn: &mut PgConnection, +) -> Result { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM mod_version_submission_comment_attachments WHERE comment_id = $1", + comment_id + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::count_attachments_for_comment failed: {e}")) + .map(|c| c.unwrap_or(0)) + .map_err(|e| e.into()) +} + +pub async fn create_attachment( + comment_id: i64, + filename: &str, + conn: &mut PgConnection, +) -> Result { + sqlx::query_as!( + ModVersionSubmissionAttachmentRow, + "INSERT INTO mod_version_submission_comment_attachments (comment_id, filename) + VALUES ($1, $2) + RETURNING id, comment_id, filename, created_at", + comment_id, + filename + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::create_attachment failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn get_attachments_for_comment( + comment_id: i64, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!( + ModVersionSubmissionAttachmentRow, + "SELECT id, comment_id, filename, created_at + FROM mod_version_submission_comment_attachments + WHERE comment_id = $1 + ORDER BY id ASC", + comment_id + ) + .fetch_all(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::get_attachments_for_comment failed: {e}")) + .map_err(|e: Error| e.into()) +} + +pub async fn get_attachment( + attachment_id: i64, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!( + ModVersionSubmissionAttachmentRow, + "SELECT id, comment_id, filename, created_at + FROM mod_version_submission_comment_attachments + WHERE id = $1", + attachment_id + ) + .fetch_optional(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::get_attachment failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn delete_attachment( + attachment_id: i64, + conn: &mut PgConnection, +) -> Result { + let result = sqlx::query!( + "DELETE FROM mod_version_submission_comment_attachments WHERE id = $1", + attachment_id + ) + .execute(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::delete_attachment failed: {e}"))?; + Ok(result.rows_affected() > 0) +} + +pub async fn count_references_to_filename( + filename: &str, + conn: &mut PgConnection, +) -> Result { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM mod_version_submission_comment_attachments WHERE filename = $1", + filename + ) + .fetch_one(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::count_references_to_filename failed: {e}")) + .map(|c| c.unwrap_or(0)) + .map_err(|e| e.into()) +} + diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 5d39d5b0..1fbc30bc 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -1,7 +1,9 @@ -use std::collections::HashMap; +use actix_multipart::Multipart; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; +use futures::StreamExt; use serde::Deserialize; use sqlx::PgConnection; +use std::collections::HashMap; use utoipa::IntoParams; use super::ApiError; @@ -10,8 +12,8 @@ use crate::database::repository::{developers, mod_version_submissions, mod_versi use crate::extractors::auth::Auth; use crate::types::api::{ApiResponse, PaginatedData}; use crate::types::models::mod_version_submission::{ - CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionComment, UpdateCommentPayload, - UpdateSubmissionPayload, + CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, + ModVersionSubmissionComment, UpdateCommentPayload, UpdateSubmissionPayload, }; #[derive(Deserialize, IntoParams)] @@ -27,6 +29,14 @@ struct CommentPath { comment_id: i64, } +#[derive(Deserialize, IntoParams)] +struct AttachmentPath { + id: String, + version: String, + comment_id: i64, + attachment_id: i64, +} + #[derive(Deserialize, IntoParams)] struct CommentsQuery { page: Option, @@ -130,11 +140,7 @@ pub async fn update_submission( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - let locked_by_id = if payload.locked { - Some(dev.id) - } else { - None - }; + let locked_by_id = if payload.locked { Some(dev.id) } else { None }; let row = mod_version_submissions::set_locked(version_id, payload.locked, locked_by_id, &mut pool) @@ -203,7 +209,7 @@ pub async fn get_comments( ids }; - let mut authors_map = developers::get_many_by_id(&author_ids, &mut pool) + let authors_map = developers::get_many_by_id(&author_ids, &mut pool) .await? .into_iter() .map(|dev| (dev.id, dev)) @@ -366,8 +372,6 @@ pub async fn update_comment( })) } -// ── DELETE comment ──────────────────────────────────────────────────────────── - /// Delete a comment on a mod version submission #[utoipa::path( delete, @@ -427,3 +431,279 @@ pub async fn delete_comment( Ok(HttpResponse::NoContent()) } + +/// List attachments for a submission comment +#[utoipa::path( + get, + path = "/v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments", + tag = "mod_version_submissions", + params(CommentPath), + responses( + (status = 200, description = "List of attachments", body = inline(ApiResponse>)), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Mod, version, submission, or comment not found"), + ), + security(("bearer_token" = [])) +)] +#[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] +pub async fn get_attachments( + path: web::Path, + data: web::Data, + auth: Auth, +) -> Result { + auth.developer()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; + + if comment_row.submission_id != version_id { + return Err(ApiError::NotFound(format!( + "Comment {} does not belong to this submission", + path.comment_id + ))); + } + + let rows = + mod_version_submissions::get_attachments_for_comment(path.comment_id, &mut pool).await?; + + let app_url = data.app_url().to_string(); + let attachments: Vec = rows + .into_iter() + .map(|r| r.into_attachment(&app_url)) + .collect(); + + Ok(web::Json(ApiResponse { + error: "".into(), + payload: attachments, + })) +} + +/// Upload attachments to a submission comment +#[utoipa::path( + post, + path = "/v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments", + tag = "mod_version_submissions", + params(CommentPath), + responses( + (status = 201, description = "Attachments uploaded", body = inline(ApiResponse>)), + (status = 400, description = "Bad request - no images, file too large, or attachment limit exceeded"), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Mod, version, submission, or comment not found"), + ), + security(("bearer_token" = [])) +)] +#[post("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] +pub async fn upload_attachments( + path: web::Path, + data: web::Data, + mut multipart: Multipart, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + if submission.locked && !dev.admin { + return Err(ApiError::BadRequest( + "Submission is locked; attachments cannot be uploaded".into(), + )); + } + + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; + + if comment_row.submission_id != version_id { + return Err(ApiError::NotFound(format!( + "Comment {} does not belong to this submission", + path.comment_id + ))); + } + + if !dev.admin + && !developers::has_access_to_mod(dev.id, &path.id, &mut pool).await? + && comment_row.author_id != dev.id + { + return Err(ApiError::Authorization); + } + + // Collect all `image` fields from the multipart stream + const MAX_BYTES: usize = 5 * 1024 * 1024; + let mut images: Vec = Vec::new(); + while let Some(field) = multipart.next().await { + let mut field = field.map_err(|e| ApiError::BadRequest(e.to_string()))?; + if field.name() != Some("image") { + continue; + } + let mut buf = bytes::BytesMut::new(); + while let Some(chunk) = field.next().await { + let chunk = chunk.map_err(|e| ApiError::BadRequest(e.to_string()))?; + buf.extend_from_slice(&chunk); + if buf.len() > MAX_BYTES { + return Err(ApiError::BadRequest("Image exceeds 5 MB limit".into())); + } + } + images.push(buf.freeze()); + } + + if images.is_empty() { + return Err(ApiError::BadRequest( + "At least one image field is required".into(), + )); + } + + let existing = + mod_version_submissions::count_attachments_for_comment(path.comment_id, &mut pool).await?; + if existing + images.len() as i64 > 5 { + return Err(ApiError::BadRequest(format!( + "Comment already has {} attachment(s); adding {} would exceed the limit of 5", + existing, + images.len() + ))); + } + + let storage_path = data.storage_path().to_string(); + + // Decode → encode WebP → hash, all in a blocking thread + let processed: Vec<(String, Vec)> = + tokio::task::spawn_blocking(move || -> Result)>, ApiError> { + images + .into_iter() + .map(|raw| { + let img = image::load_from_memory(&raw) + .map_err(|e| ApiError::BadRequest(format!("Invalid image: {e}")))?; + let mut webp_bytes: Vec = Vec::new(); + img.write_to( + &mut std::io::Cursor::new(&mut webp_bytes), + image::ImageFormat::WebP, + ) + .map_err(|e| ApiError::InternalError(format!("WebP encode failed: {e}")))?; + let filename = format!("{}.webp", sha256::digest(&webp_bytes)); + Ok((filename, webp_bytes)) + }) + .collect() + }) + .await + .map_err(|e| ApiError::InternalError(format!("Task join error: {e}")))??; + + // Write files and insert DB rows + let attachments_dir = format!("{}/submission_attachments", storage_path); + let app_url = data.app_url().to_string(); + let mut result = Vec::with_capacity(processed.len()); + for (filename, webp_bytes) in processed { + let file_path = format!("{}/{}", attachments_dir, filename); + if !std::path::Path::new(&file_path).exists() { + tokio::fs::write(&file_path, &webp_bytes) + .await + .map_err(|e| ApiError::InternalError(format!("Failed to write file: {e}")))?; + } + let row = mod_version_submissions::create_attachment(path.comment_id, &filename, &mut pool) + .await?; + result.push(row.into_attachment(&app_url)); + } + + Ok(HttpResponse::Created().json(ApiResponse { + error: "".into(), + payload: result, + })) +} + +/// Delete an attachment from a submission comment +#[utoipa::path( + delete, + path = "/v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments/{attachment_id}", + tag = "mod_version_submissions", + params(AttachmentPath), + responses( + (status = 204, description = "Attachment deleted"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Mod, version, submission, comment, or attachment not found"), + ), + security(("bearer_token" = [])) +)] +#[delete( + "v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments/{attachment_id}" +)] +pub async fn delete_attachment( + path: web::Path, + data: web::Data, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; + + if comment_row.submission_id != version_id { + return Err(ApiError::NotFound(format!( + "Comment {} does not belong to this submission", + path.comment_id + ))); + } + + if !dev.admin && comment_row.author_id != dev.id { + return Err(ApiError::Authorization); + } + + let attachment = mod_version_submissions::get_attachment(path.attachment_id, &mut pool) + .await? + .ok_or_else(|| { + ApiError::NotFound(format!("Attachment {} not found", path.attachment_id)) + })?; + + if attachment.comment_id != path.comment_id { + return Err(ApiError::NotFound(format!( + "Attachment {} does not belong to this comment", + path.attachment_id + ))); + } + + let filename = attachment.filename.clone(); + mod_version_submissions::delete_attachment(path.attachment_id, &mut pool).await?; + + let remaining = + mod_version_submissions::count_references_to_filename(&filename, &mut pool).await?; + if remaining == 0 { + let file_path = format!("{}/submission_attachments/{}", data.storage_path(), filename); + tokio::fs::remove_file(&file_path).await.ok(); + } + + Ok(HttpResponse::NoContent()) +} diff --git a/src/main.rs b/src/main.rs index 8757ca3f..402bb164 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,10 @@ async fn main() -> anyhow::Result<()> { log::info!("Running migrations"); sqlx::migrate!("./migrations").run(app_data.db()).await?; + let attachments_dir = format!("{}/submission_attachments", app_data.storage_path()); + std::fs::create_dir_all(&attachments_dir) + .map_err(|e| anyhow::anyhow!("Failed to create storage directory: {e}"))?; + let port = app_data.port(); let debug = app_data.debug(); @@ -75,6 +79,9 @@ async fn main() -> anyhow::Result<()> { .service(endpoints::mod_version_submissions::create_comment) .service(endpoints::mod_version_submissions::update_comment) .service(endpoints::mod_version_submissions::delete_comment) + .service(endpoints::mod_version_submissions::get_attachments) + .service(endpoints::mod_version_submissions::upload_attachments) + .service(endpoints::mod_version_submissions::delete_attachment) .service(endpoints::deprecations::index) .service(endpoints::deprecations::store) .service(endpoints::deprecations::update) diff --git a/src/openapi.rs b/src/openapi.rs index 77dd0903..6c976737 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -22,6 +22,9 @@ use crate::{endpoints, types}; endpoints::mod_version_submissions::create_comment, endpoints::mod_version_submissions::update_comment, endpoints::mod_version_submissions::delete_comment, + endpoints::mod_version_submissions::get_attachments, + endpoints::mod_version_submissions::upload_attachments, + endpoints::mod_version_submissions::delete_attachment, endpoints::deprecations::index, endpoints::deprecations::store, endpoints::deprecations::update, @@ -83,6 +86,7 @@ use crate::{endpoints, types}; types::models::mod_version_submission::UpdateSubmissionPayload, types::models::mod_version_submission::CreateCommentPayload, types::models::mod_version_submission::UpdateCommentPayload, + types::models::mod_version_submission::ModVersionSubmissionAttachment, endpoints::mods::IndexSortType, endpoints::developers::SimpleDevMod, endpoints::developers::SimpleDevModVersion, diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index 4cd224fc..7c4cbc98 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -64,6 +64,35 @@ impl ModVersionSubmissionCommentRow { } } +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct ModVersionSubmissionAttachment { + pub id: i64, + pub comment_id: i64, + pub url: String, + pub created_at: DateTime, +} + +pub struct ModVersionSubmissionAttachmentRow { + pub id: i64, + pub comment_id: i64, + pub filename: String, + pub created_at: DateTime, +} + +impl ModVersionSubmissionAttachmentRow { + pub fn into_attachment(self, app_url: &str) -> ModVersionSubmissionAttachment { + ModVersionSubmissionAttachment { + id: self.id, + comment_id: self.comment_id, + url: format!( + "{}/storage/submission-attachments/{}", + app_url, self.filename + ), + created_at: self.created_at, + } + } +} + #[derive(Deserialize, ToSchema)] pub struct UpdateSubmissionPayload { pub locked: bool, From 17486fe9bfb83c84d37f1cb25526c0a3393000bf Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 12 Mar 2026 23:39:26 +0200 Subject: [PATCH 04/69] fix: sanitize comments --- Cargo.lock | 225 +++++++++++++++++++++ Cargo.toml | 1 + src/endpoints/mod_version_submissions.rs | 23 ++- src/types/models/mod_version_submission.rs | 4 + 4 files changed, 251 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc3bf5c6..0e729a42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,6 +303,19 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ammonia" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" +dependencies = [ + "cssparser", + "html5ever", + "maplit", + "tendril", + "url", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -958,6 +971,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "darling" version = "0.20.11" @@ -1101,6 +1137,21 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1298,6 +1349,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -1415,6 +1476,7 @@ dependencies = [ "actix-cors", "actix-multipart", "actix-web", + "ammonia", "anyhow", "bytes", "chrono", @@ -1573,6 +1635,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "0.2.12" @@ -2171,6 +2244,40 @@ dependencies = [ "sha2", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2475,6 +2582,58 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2563,6 +2722,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -3311,6 +3476,12 @@ dependencies = [ "quote", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.11" @@ -3569,6 +3740,31 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -3642,6 +3838,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4012,6 +4219,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4234,6 +4447,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webpki-root-certs" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index e9e8ccaf..bcea2554 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,4 @@ moka = { version = "0.12.13", features = ["future"] } utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "uuid"] } utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web"] } bytes = "1.11" +ammonia = "4" diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 1fbc30bc..f61bd19e 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -8,6 +8,15 @@ use utoipa::IntoParams; use super::ApiError; use crate::config::AppData; + +fn sanitize_comment(raw: &str) -> String { + ammonia::Builder::default() + .tags(std::collections::HashSet::new()) + .clean(raw) + .to_string() + .trim() + .to_string() +} use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; use crate::extractors::auth::Auth; use crate::types::api::{ApiResponse, PaginatedData}; @@ -259,10 +268,15 @@ pub async fn create_comment( ) -> Result { let dev = auth.developer()?; - let comment_text = payload.comment.trim().to_string(); + let comment_text = sanitize_comment(&payload.comment); if comment_text.is_empty() { return Err(ApiError::BadRequest("Comment must not be empty".into())); } + if comment_text.len() > 1000 { + return Err(ApiError::BadRequest( + "Comment must not exceed 1000 characters".into(), + )); + } let mut pool = data.db().acquire().await?; @@ -321,10 +335,15 @@ pub async fn update_comment( ) -> Result { let dev = auth.developer()?; - let comment_text = payload.comment.trim().to_string(); + let comment_text = sanitize_comment(&payload.comment); if comment_text.is_empty() { return Err(ApiError::BadRequest("Comment must not be empty".into())); } + if comment_text.len() > 1000 { + return Err(ApiError::BadRequest( + "Comment must not exceed 1000 characters".into(), + )); + } let mut pool = data.db().acquire().await?; diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index 7c4cbc98..b06e5d0d 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -100,11 +100,15 @@ pub struct UpdateSubmissionPayload { #[derive(Deserialize, ToSchema)] pub struct CreateCommentPayload { + #[schema(max_length = 1000)] + /// Plain text comment; HTML tags are stripped pub comment: String, } #[derive(Deserialize, ToSchema)] pub struct UpdateCommentPayload { + #[schema(max_length = 1000)] + /// Plain text comment; HTML tags are stripped pub comment: String, } From 8cb554b2da272e00a82d7dcb260f8bce5c8ef4e4 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 12 Mar 2026 23:40:59 +0200 Subject: [PATCH 05/69] fix: uhhh formatting --- src/endpoints/mod_version_submissions.rs | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index f61bd19e..31dfa04b 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -1,3 +1,12 @@ +use super::ApiError; +use crate::config::AppData; +use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; +use crate::extractors::auth::Auth; +use crate::types::api::{ApiResponse, PaginatedData}; +use crate::types::models::mod_version_submission::{ + CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, + ModVersionSubmissionComment, UpdateCommentPayload, UpdateSubmissionPayload, +}; use actix_multipart::Multipart; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use futures::StreamExt; @@ -6,9 +15,6 @@ use sqlx::PgConnection; use std::collections::HashMap; use utoipa::IntoParams; -use super::ApiError; -use crate::config::AppData; - fn sanitize_comment(raw: &str) -> String { ammonia::Builder::default() .tags(std::collections::HashSet::new()) @@ -17,13 +23,6 @@ fn sanitize_comment(raw: &str) -> String { .trim() .to_string() } -use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; -use crate::extractors::auth::Auth; -use crate::types::api::{ApiResponse, PaginatedData}; -use crate::types::models::mod_version_submission::{ - CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, - ModVersionSubmissionComment, UpdateCommentPayload, UpdateSubmissionPayload, -}; #[derive(Deserialize, IntoParams)] struct SubmissionPath { @@ -720,7 +719,11 @@ pub async fn delete_attachment( let remaining = mod_version_submissions::count_references_to_filename(&filename, &mut pool).await?; if remaining == 0 { - let file_path = format!("{}/submission_attachments/{}", data.storage_path(), filename); + let file_path = format!( + "{}/submission_attachments/{}", + data.storage_path(), + filename + ); tokio::fs::remove_file(&file_path).await.ok(); } From d00aac9c1fa5113dbdc157300f6832965b037cda Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 12 Mar 2026 23:46:11 +0200 Subject: [PATCH 06/69] chore: sqlx prepare --- ...ac1a75139bf9937a76d542e0fc3361fe156d1.json | 22 ++++++++ ...85b2bc796c64f42f15f078a5639f717aa21b9.json | 40 ++++++++++++++ ...0af4628f2b5803a59b2c28c6052250d60bef9.json | 46 ++++++++++++++++ ...084ef1440d4a166a1296bedf9529395272486.json | 54 +++++++++++++++++++ ...ed41b993407ae32bf0e90fbdd2c8ca3ea9d22.json | 41 ++++++++++++++ ...c8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json | 48 +++++++++++++++++ ...6299e3e19eee7b530be498c609e6cd6fc2b33.json | 40 ++++++++++++++ ...cc3566512dc2273dc3b72ba1ba5c2d281aeb9.json | 22 ++++++++ ...daf4b82d907eba99837083e21909ea9c195a7.json | 52 ++++++++++++++++++ ...bc4cda1f108a749cc953adb9d25a2fffa4072.json | 53 ++++++++++++++++++ ...27118f0a35841049fc0914add705241d471f6.json | 22 ++++++++ ...f10f6cbb224587556161b25507d5e56cdbb46.json | 14 +++++ ...b2593d77d4d24d6e2d9ba052ba4bf1cc6489d.json | 52 ++++++++++++++++++ ...7bc3b731d8ecc313548af8f9afcd523fa6443.json | 46 ++++++++++++++++ ...8a56617000567bfd04ed8f9be0e501f438937.json | 54 +++++++++++++++++++ ...5fa8cffd29bd82f9ee848b95b11b1f9f03777.json | 14 +++++ 16 files changed, 620 insertions(+) create mode 100644 .sqlx/query-0411ef36ab03892ea4fcf4bb8e7ac1a75139bf9937a76d542e0fc3361fe156d1.json create mode 100644 .sqlx/query-0519d0d7c6d97ffbff763ba2d1c85b2bc796c64f42f15f078a5639f717aa21b9.json create mode 100644 .sqlx/query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json create mode 100644 .sqlx/query-1ee95124185a80b20fffcc2ca64084ef1440d4a166a1296bedf9529395272486.json create mode 100644 .sqlx/query-4ad2b61d2fa7a7edc9ec5ec68eaed41b993407ae32bf0e90fbdd2c8ca3ea9d22.json create mode 100644 .sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json create mode 100644 .sqlx/query-75645a63e093cdaadb0ba84844e6299e3e19eee7b530be498c609e6cd6fc2b33.json create mode 100644 .sqlx/query-7a9ffa7b59f3550e2bd38c756b9cc3566512dc2273dc3b72ba1ba5c2d281aeb9.json create mode 100644 .sqlx/query-8affc6971c1e33b14f3696d0bd9daf4b82d907eba99837083e21909ea9c195a7.json create mode 100644 .sqlx/query-8c1f2dd19652bb6c393d83c6443bc4cda1f108a749cc953adb9d25a2fffa4072.json create mode 100644 .sqlx/query-91bc425cc6b35915fc00d39b1f127118f0a35841049fc0914add705241d471f6.json create mode 100644 .sqlx/query-9eebe36dd30154f9ca5943404eff10f6cbb224587556161b25507d5e56cdbb46.json create mode 100644 .sqlx/query-bad99b175dfe2ffccfc71b69175b2593d77d4d24d6e2d9ba052ba4bf1cc6489d.json create mode 100644 .sqlx/query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json create mode 100644 .sqlx/query-ddcfdb20c8c93a41da225a937478a56617000567bfd04ed8f9be0e501f438937.json create mode 100644 .sqlx/query-ea5cf95e89891cf0650305824775fa8cffd29bd82f9ee848b95b11b1f9f03777.json diff --git a/.sqlx/query-0411ef36ab03892ea4fcf4bb8e7ac1a75139bf9937a76d542e0fc3361fe156d1.json b/.sqlx/query-0411ef36ab03892ea4fcf4bb8e7ac1a75139bf9937a76d542e0fc3361fe156d1.json new file mode 100644 index 00000000..0bb81ef3 --- /dev/null +++ b/.sqlx/query-0411ef36ab03892ea4fcf4bb8e7ac1a75139bf9937a76d542e0fc3361fe156d1.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM mod_version_submission_comments WHERE submission_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "0411ef36ab03892ea4fcf4bb8e7ac1a75139bf9937a76d542e0fc3361fe156d1" +} diff --git a/.sqlx/query-0519d0d7c6d97ffbff763ba2d1c85b2bc796c64f42f15f078a5639f717aa21b9.json b/.sqlx/query-0519d0d7c6d97ffbff763ba2d1c85b2bc796c64f42f15f078a5639f717aa21b9.json new file mode 100644 index 00000000..0ceae2aa --- /dev/null +++ b/.sqlx/query-0519d0d7c6d97ffbff763ba2d1c85b2bc796c64f42f15f078a5639f717aa21b9.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, comment_id, filename, created_at\n FROM mod_version_submission_comment_attachments\n WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "comment_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "filename", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "0519d0d7c6d97ffbff763ba2d1c85b2bc796c64f42f15f078a5639f717aa21b9" +} diff --git a/.sqlx/query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json b/.sqlx/query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json new file mode 100644 index 00000000..7ed59aa9 --- /dev/null +++ b/.sqlx/query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n mod_version_id, locked, locked_by,\n created_at, updated_at\n FROM mod_version_submissions\n WHERE mod_version_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_version_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "locked", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "locked_by", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9" +} diff --git a/.sqlx/query-1ee95124185a80b20fffcc2ca64084ef1440d4a166a1296bedf9529395272486.json b/.sqlx/query-1ee95124185a80b20fffcc2ca64084ef1440d4a166a1296bedf9529395272486.json new file mode 100644 index 00000000..5b67d135 --- /dev/null +++ b/.sqlx/query-1ee95124185a80b20fffcc2ca64084ef1440d4a166a1296bedf9529395272486.json @@ -0,0 +1,54 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n id, submission_id, comment, author_id,\n created_at, updated_at\n FROM mod_version_submission_comments\n WHERE submission_id = $1\n ORDER BY id DESC\n LIMIT $2 OFFSET $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "submission_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "comment", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "author_id", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "1ee95124185a80b20fffcc2ca64084ef1440d4a166a1296bedf9529395272486" +} diff --git a/.sqlx/query-4ad2b61d2fa7a7edc9ec5ec68eaed41b993407ae32bf0e90fbdd2c8ca3ea9d22.json b/.sqlx/query-4ad2b61d2fa7a7edc9ec5ec68eaed41b993407ae32bf0e90fbdd2c8ca3ea9d22.json new file mode 100644 index 00000000..4d84f0d3 --- /dev/null +++ b/.sqlx/query-4ad2b61d2fa7a7edc9ec5ec68eaed41b993407ae32bf0e90fbdd2c8ca3ea9d22.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_version_submission_comment_attachments (comment_id, filename)\n VALUES ($1, $2)\n RETURNING id, comment_id, filename, created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "comment_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "filename", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "4ad2b61d2fa7a7edc9ec5ec68eaed41b993407ae32bf0e90fbdd2c8ca3ea9d22" +} diff --git a/.sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json b/.sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json new file mode 100644 index 00000000..4c06ced0 --- /dev/null +++ b/.sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE mod_version_submissions\n SET locked = $1, locked_by = $2, updated_at = NOW()\n WHERE mod_version_id = $3\n RETURNING mod_version_id, locked, locked_by, created_at, updated_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_version_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "locked", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "locked_by", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Bool", + "Int4", + "Int4" + ] + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c" +} diff --git a/.sqlx/query-75645a63e093cdaadb0ba84844e6299e3e19eee7b530be498c609e6cd6fc2b33.json b/.sqlx/query-75645a63e093cdaadb0ba84844e6299e3e19eee7b530be498c609e6cd6fc2b33.json new file mode 100644 index 00000000..0b4f3933 --- /dev/null +++ b/.sqlx/query-75645a63e093cdaadb0ba84844e6299e3e19eee7b530be498c609e6cd6fc2b33.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, comment_id, filename, created_at\n FROM mod_version_submission_comment_attachments\n WHERE comment_id = $1\n ORDER BY id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "comment_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "filename", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "75645a63e093cdaadb0ba84844e6299e3e19eee7b530be498c609e6cd6fc2b33" +} diff --git a/.sqlx/query-7a9ffa7b59f3550e2bd38c756b9cc3566512dc2273dc3b72ba1ba5c2d281aeb9.json b/.sqlx/query-7a9ffa7b59f3550e2bd38c756b9cc3566512dc2273dc3b72ba1ba5c2d281aeb9.json new file mode 100644 index 00000000..568b54b1 --- /dev/null +++ b/.sqlx/query-7a9ffa7b59f3550e2bd38c756b9cc3566512dc2273dc3b72ba1ba5c2d281aeb9.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM mod_version_submission_comment_attachments WHERE filename = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "7a9ffa7b59f3550e2bd38c756b9cc3566512dc2273dc3b72ba1ba5c2d281aeb9" +} diff --git a/.sqlx/query-8affc6971c1e33b14f3696d0bd9daf4b82d907eba99837083e21909ea9c195a7.json b/.sqlx/query-8affc6971c1e33b14f3696d0bd9daf4b82d907eba99837083e21909ea9c195a7.json new file mode 100644 index 00000000..2ca4f752 --- /dev/null +++ b/.sqlx/query-8affc6971c1e33b14f3696d0bd9daf4b82d907eba99837083e21909ea9c195a7.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n id,\n username,\n display_name,\n verified,\n admin,\n github_user_id as github_id\n FROM developers\n WHERE id = ANY($1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "verified", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "admin", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "github_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "8affc6971c1e33b14f3696d0bd9daf4b82d907eba99837083e21909ea9c195a7" +} diff --git a/.sqlx/query-8c1f2dd19652bb6c393d83c6443bc4cda1f108a749cc953adb9d25a2fffa4072.json b/.sqlx/query-8c1f2dd19652bb6c393d83c6443bc4cda1f108a749cc953adb9d25a2fffa4072.json new file mode 100644 index 00000000..4f049ef8 --- /dev/null +++ b/.sqlx/query-8c1f2dd19652bb6c393d83c6443bc4cda1f108a749cc953adb9d25a2fffa4072.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE mod_version_submission_comments\n SET comment = $1, updated_at = NOW()\n WHERE id = $2\n RETURNING id, submission_id, comment, author_id, created_at, updated_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "submission_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "comment", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "author_id", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "8c1f2dd19652bb6c393d83c6443bc4cda1f108a749cc953adb9d25a2fffa4072" +} diff --git a/.sqlx/query-91bc425cc6b35915fc00d39b1f127118f0a35841049fc0914add705241d471f6.json b/.sqlx/query-91bc425cc6b35915fc00d39b1f127118f0a35841049fc0914add705241d471f6.json new file mode 100644 index 00000000..4f45f9bb --- /dev/null +++ b/.sqlx/query-91bc425cc6b35915fc00d39b1f127118f0a35841049fc0914add705241d471f6.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM mod_version_submission_comment_attachments WHERE comment_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "91bc425cc6b35915fc00d39b1f127118f0a35841049fc0914add705241d471f6" +} diff --git a/.sqlx/query-9eebe36dd30154f9ca5943404eff10f6cbb224587556161b25507d5e56cdbb46.json b/.sqlx/query-9eebe36dd30154f9ca5943404eff10f6cbb224587556161b25507d5e56cdbb46.json new file mode 100644 index 00000000..5b672920 --- /dev/null +++ b/.sqlx/query-9eebe36dd30154f9ca5943404eff10f6cbb224587556161b25507d5e56cdbb46.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM mod_version_submission_comment_attachments WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "9eebe36dd30154f9ca5943404eff10f6cbb224587556161b25507d5e56cdbb46" +} diff --git a/.sqlx/query-bad99b175dfe2ffccfc71b69175b2593d77d4d24d6e2d9ba052ba4bf1cc6489d.json b/.sqlx/query-bad99b175dfe2ffccfc71b69175b2593d77d4d24d6e2d9ba052ba4bf1cc6489d.json new file mode 100644 index 00000000..f983d45f --- /dev/null +++ b/.sqlx/query-bad99b175dfe2ffccfc71b69175b2593d77d4d24d6e2d9ba052ba4bf1cc6489d.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, submission_id, comment, author_id, created_at, updated_at\n FROM mod_version_submission_comments\n WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "submission_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "comment", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "author_id", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "bad99b175dfe2ffccfc71b69175b2593d77d4d24d6e2d9ba052ba4bf1cc6489d" +} diff --git a/.sqlx/query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json b/.sqlx/query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json new file mode 100644 index 00000000..9a6bda4d --- /dev/null +++ b/.sqlx/query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_version_submissions (mod_version_id)\n VALUES ($1)\n RETURNING mod_version_id, locked, locked_by, created_at, updated_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_version_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "locked", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "locked_by", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443" +} diff --git a/.sqlx/query-ddcfdb20c8c93a41da225a937478a56617000567bfd04ed8f9be0e501f438937.json b/.sqlx/query-ddcfdb20c8c93a41da225a937478a56617000567bfd04ed8f9be0e501f438937.json new file mode 100644 index 00000000..644bfe33 --- /dev/null +++ b/.sqlx/query-ddcfdb20c8c93a41da225a937478a56617000567bfd04ed8f9be0e501f438937.json @@ -0,0 +1,54 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_version_submission_comments (submission_id, author_id, comment)\n VALUES ($1, $2, $3)\n RETURNING id, submission_id, comment, author_id, created_at, updated_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "submission_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "comment", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "author_id", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int4", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "ddcfdb20c8c93a41da225a937478a56617000567bfd04ed8f9be0e501f438937" +} diff --git a/.sqlx/query-ea5cf95e89891cf0650305824775fa8cffd29bd82f9ee848b95b11b1f9f03777.json b/.sqlx/query-ea5cf95e89891cf0650305824775fa8cffd29bd82f9ee848b95b11b1f9f03777.json new file mode 100644 index 00000000..ad6b709b --- /dev/null +++ b/.sqlx/query-ea5cf95e89891cf0650305824775fa8cffd29bd82f9ee848b95b11b1f9f03777.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM mod_version_submission_comments WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "ea5cf95e89891cf0650305824775fa8cffd29bd82f9ee848b95b11b1f9f03777" +} From 8334ffb4ef2399f055d1401fc7f8fe915823745c Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 13 Mar 2026 00:37:56 +0200 Subject: [PATCH 07/69] fix: fix some permission issues --- src/endpoints/mod_version_submissions.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 31dfa04b..261b54ae 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -544,7 +544,7 @@ pub async fn upload_attachments( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if submission.locked && !dev.admin { + if submission.locked { return Err(ApiError::BadRequest( "Submission is locked; attachments cannot be uploaded".into(), )); @@ -561,10 +561,7 @@ pub async fn upload_attachments( ))); } - if !dev.admin - && !developers::has_access_to_mod(dev.id, &path.id, &mut pool).await? - && comment_row.author_id != dev.id - { + if comment_row.author_id != dev.id { return Err(ApiError::Authorization); } @@ -681,10 +678,16 @@ pub async fn delete_attachment( let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; - mod_version_submissions::get_for_mod_version(version_id, &mut pool) + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + if submission.locked && !dev.admin { + return Err(ApiError::BadRequest( + "Submission is locked; attachments cannot be deleted".into(), + )); + } + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) .await? .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; From fcb305877d2bb8258ea882b22c26dacf49eb7db2 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 25 Mar 2026 00:26:16 +0200 Subject: [PATCH 08/69] chore: remove comment --- src/endpoints/mod_version_submissions.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 261b54ae..6bbfd6c3 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -624,7 +624,6 @@ pub async fn upload_attachments( .await .map_err(|e| ApiError::InternalError(format!("Task join error: {e}")))??; - // Write files and insert DB rows let attachments_dir = format!("{}/submission_attachments", storage_path); let app_url = data.app_url().to_string(); let mut result = Vec::with_capacity(processed.len()); From 72e85621527303fd23228eb78e3fdbd918c6e261 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 15 Apr 2026 00:04:18 +0300 Subject: [PATCH 09/69] fix: storage fixes --- ...eate_submission_comment_attachments.up.sql | 2 +- .../repository/mod_version_submissions.rs | 59 +++++++++- src/endpoints/mod.rs | 2 + src/endpoints/mod_version_submissions.rs | 109 +++++++++++------- src/main.rs | 7 +- src/storage.rs | 9 +- 6 files changed, 133 insertions(+), 55 deletions(-) diff --git a/migrations/20260312205413_create_submission_comment_attachments.up.sql b/migrations/20260312205413_create_submission_comment_attachments.up.sql index 6b6ea1d3..7dfc769f 100644 --- a/migrations/20260312205413_create_submission_comment_attachments.up.sql +++ b/migrations/20260312205413_create_submission_comment_attachments.up.sql @@ -3,7 +3,7 @@ CREATE TABLE mod_version_submission_comment_attachments ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, comment_id BIGINT NOT NULL, - filename TEXT NOT NULL, -- SHA-256 hex of WebP content; file on disk: {STORAGE_PATH}/submission_attachments/{filename} + filename TEXT NOT NULL, -- SHA-256 hex of WebP content; file on disk: {STORAGE_PATH}/submission-attachments/{filename} created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (comment_id) REFERENCES mod_version_submission_comments (id) ON DELETE CASCADE ); diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index dc30f731..a1a119c0 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -3,6 +3,7 @@ use crate::types::models::mod_version_submission::{ ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionRow, }; use sqlx::{Error, PgConnection}; +use std::collections::HashMap; pub async fn get_for_mod_version( id: i32, @@ -83,7 +84,9 @@ pub async fn get_paginated_comments_for_submission( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::get_paginated_items_for_submission failed: {e}")) + .inspect_err(|e| { + log::error!("mod_version_submissions::get_paginated_items_for_submission failed: {e}") + }) .map_err(|e| e.into()) } @@ -97,7 +100,9 @@ pub async fn count_comments_for_submission( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::count_comments_for_submission failed: {e}")) + .inspect_err(|e| { + log::error!("mod_version_submissions::count_comments_for_submission failed: {e}") + }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } @@ -184,7 +189,9 @@ pub async fn count_attachments_for_comment( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::count_attachments_for_comment failed: {e}")) + .inspect_err(|e| { + log::error!("mod_version_submissions::count_attachments_for_comment failed: {e}") + }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } @@ -222,7 +229,9 @@ pub async fn get_attachments_for_comment( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::get_attachments_for_comment failed: {e}")) + .inspect_err(|e| { + log::error!("mod_version_submissions::get_attachments_for_comment failed: {e}") + }) .map_err(|e: Error| e.into()) } @@ -257,6 +266,20 @@ pub async fn delete_attachment( Ok(result.rows_affected() > 0) } +pub async fn delete_attachments_for_comment( + comment_id: i64, + conn: &mut PgConnection, +) -> Result { + let result = sqlx::query!( + "DELETE FROM mod_version_submission_comment_attachments WHERE comment_id = $1", + comment_id + ) + .execute(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::delete_attachments_for_comment_id failed: {e}"))?; + Ok(result.rows_affected() > 0) +} + pub async fn count_references_to_filename( filename: &str, conn: &mut PgConnection, @@ -267,8 +290,34 @@ pub async fn count_references_to_filename( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::count_references_to_filename failed: {e}")) + .inspect_err(|e| { + log::error!("mod_version_submissions::count_references_to_filename failed: {e}") + }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } +pub async fn count_references_to_filenames( + filenames: &[String], + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query!( + "SELECT + filename, COUNT(*) as count + FROM mod_version_submission_comment_attachments + WHERE filename = ANY($1) + GROUP BY filename", + filenames + ) + .fetch_all(conn) + .await + .map(|x| { + x.into_iter() + .map(|record| (record.filename, record.count.unwrap_or(0))) + .collect() + }) + .inspect_err(|e| { + log::error!("mod_version_submissions::count_references_to_filenames failed: {e}") + }) + .map_err(|e| e.into()) +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index ca921420..91d24806 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -43,6 +43,8 @@ pub enum ApiError { Zip(#[from] zip::result::ZipError), #[error("Failed to contact external resource: {0}")] Reqwest(#[from] reqwest::Error), + #[error("I/O error: {0}")] + IO(#[from] std::io::Error), } impl ApiError { diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 6bbfd6c3..e2633ec5 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -2,6 +2,7 @@ use super::ApiError; use crate::config::AppData; use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; use crate::extractors::auth::Auth; +use crate::storage::StorageDisk; use crate::types::api::{ApiResponse, PaginatedData}; use crate::types::models::mod_version_submission::{ CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, @@ -10,8 +11,9 @@ use crate::types::models::mod_version_submission::{ use actix_multipart::Multipart; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use futures::StreamExt; +use log::error; use serde::Deserialize; -use sqlx::PgConnection; +use sqlx::{Acquire, PgConnection}; use std::collections::HashMap; use utoipa::IntoParams; @@ -414,13 +416,15 @@ pub async fn delete_comment( let mut pool = data.db().acquire().await?; - if !mods::exists(&path.id, &mut pool).await? { + let mut tx = pool.begin().await?; + + if !mods::exists(&path.id, &mut tx).await? { return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + let version_id = resolve_version_id(&path.id, &path.version, &mut tx).await?; - let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; @@ -430,7 +434,7 @@ pub async fn delete_comment( )); } - let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; @@ -445,7 +449,28 @@ pub async fn delete_comment( return Err(ApiError::Authorization); } - mod_version_submissions::delete_comment(path.comment_id, &mut pool).await?; + let attachment_filenames = + mod_version_submissions::get_attachments_for_comment(comment_row.id, &mut tx) + .await? + .into_iter() + .map(|a| a.filename) + .collect::>(); + + mod_version_submissions::delete_comment(path.comment_id, &mut tx).await?; + + let references = + mod_version_submissions::count_references_to_filenames(&attachment_filenames, &mut tx) + .await?; + + tx.commit().await?; + + for (filename, count) in references { + if count == 0 { + if let Err(e) = data.static_storage().delete(&filename).await { + error!("Failed to delete attachment file {}: {e}", filename); + } + } + } Ok(HttpResponse::NoContent()) } @@ -534,13 +559,15 @@ pub async fn upload_attachments( let mut pool = data.db().acquire().await?; - if !mods::exists(&path.id, &mut pool).await? { + let mut tx = pool.begin().await?; + + if !mods::exists(&path.id, &mut tx).await? { return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + let version_id = resolve_version_id(&path.id, &path.version, &mut tx).await?; - let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; @@ -550,7 +577,7 @@ pub async fn upload_attachments( )); } - let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; @@ -591,7 +618,7 @@ pub async fn upload_attachments( } let existing = - mod_version_submissions::count_attachments_for_comment(path.comment_id, &mut pool).await?; + mod_version_submissions::count_attachments_for_comment(path.comment_id, &mut tx).await?; if existing + images.len() as i64 > 5 { return Err(ApiError::BadRequest(format!( "Comment already has {} attachment(s); adding {} would exceed the limit of 5", @@ -600,11 +627,9 @@ pub async fn upload_attachments( ))); } - let storage_path = data.storage_path().to_string(); - // Decode → encode WebP → hash, all in a blocking thread - let processed: Vec<(String, Vec)> = - tokio::task::spawn_blocking(move || -> Result)>, ApiError> { + let processed: Vec> = + tokio::task::spawn_blocking(move || -> Result>, ApiError> { images .into_iter() .map(|raw| { @@ -616,29 +641,26 @@ pub async fn upload_attachments( image::ImageFormat::WebP, ) .map_err(|e| ApiError::InternalError(format!("WebP encode failed: {e}")))?; - let filename = format!("{}.webp", sha256::digest(&webp_bytes)); - Ok((filename, webp_bytes)) + Ok(webp_bytes) }) .collect() }) .await .map_err(|e| ApiError::InternalError(format!("Task join error: {e}")))??; - let attachments_dir = format!("{}/submission_attachments", storage_path); - let app_url = data.app_url().to_string(); let mut result = Vec::with_capacity(processed.len()); - for (filename, webp_bytes) in processed { - let file_path = format!("{}/{}", attachments_dir, filename); - if !std::path::Path::new(&file_path).exists() { - tokio::fs::write(&file_path, &webp_bytes) - .await - .map_err(|e| ApiError::InternalError(format!("Failed to write file: {e}")))?; - } - let row = mod_version_submissions::create_attachment(path.comment_id, &filename, &mut pool) + for webp_bytes in &processed { + let filename = data + .static_storage() + .store_hashed_with_extension("submission-attachments", webp_bytes, Some("webp")) .await?; - result.push(row.into_attachment(&app_url)); + let row = + mod_version_submissions::create_attachment(path.comment_id, &filename, &mut tx).await?; + result.push(row.into_attachment(&data.app_url().to_string())); } + tx.commit().await?; + Ok(HttpResponse::Created().json(ApiResponse { error: "".into(), payload: result, @@ -671,13 +693,15 @@ pub async fn delete_attachment( let mut pool = data.db().acquire().await?; - if !mods::exists(&path.id, &mut pool).await? { + let mut tx = pool.begin().await?; + + if !mods::exists(&path.id, &mut tx).await? { return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + let version_id = resolve_version_id(&path.id, &path.version, &mut tx).await?; - let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; @@ -687,7 +711,7 @@ pub async fn delete_attachment( )); } - let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; @@ -702,7 +726,7 @@ pub async fn delete_attachment( return Err(ApiError::Authorization); } - let attachment = mod_version_submissions::get_attachment(path.attachment_id, &mut pool) + let attachment = mod_version_submissions::get_attachment(path.attachment_id, &mut tx) .await? .ok_or_else(|| { ApiError::NotFound(format!("Attachment {} not found", path.attachment_id)) @@ -716,17 +740,20 @@ pub async fn delete_attachment( } let filename = attachment.filename.clone(); - mod_version_submissions::delete_attachment(path.attachment_id, &mut pool).await?; + mod_version_submissions::delete_attachment(path.attachment_id, &mut tx).await?; let remaining = - mod_version_submissions::count_references_to_filename(&filename, &mut pool).await?; + mod_version_submissions::count_references_to_filename(&filename, &mut tx).await?; + + tx.commit().await?; + if remaining == 0 { - let file_path = format!( - "{}/submission_attachments/{}", - data.storage_path(), - filename - ); - tokio::fs::remove_file(&file_path).await.ok(); + // TODO: implement a cleanup job for orphaned attachment files + // If this fails it's fine to still return a 204 to the user + let r = data.static_storage().delete(&filename).await; + if let Err(e) = r { + error!("Failed to delete attachment: {e}"); + } } Ok(HttpResponse::NoContent()) diff --git a/src/main.rs b/src/main.rs index 2e219c34..07972e27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use actix_web::{ }; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use crate::storage::StorageDisk; mod auth; mod cli; @@ -29,6 +30,8 @@ async fn main() -> anyhow::Result<()> { .map_err(|e| e.context("Failed to read log4rs config"))?; let app_data = config::build_config().await?; + app_data.static_storage().init().await?; + app_data.private_storage().init().await?; if cli::maybe_cli(&app_data).await? { return Ok(()); @@ -37,10 +40,6 @@ async fn main() -> anyhow::Result<()> { log::info!("Running migrations"); sqlx::migrate!("./migrations").run(app_data.db()).await?; - let attachments_dir = format!("{}/submission_attachments", app_data.storage_path()); - std::fs::create_dir_all(&attachments_dir) - .map_err(|e| anyhow::anyhow!("Failed to create storage directory: {e}"))?; - let port = app_data.port(); let debug = app_data.debug(); diff --git a/src/storage.rs b/src/storage.rs index d8ab9770..c74c4e68 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -38,7 +38,7 @@ impl StorageDisk for PrivateStorage { } } -trait StorageDisk { +pub trait StorageDisk { async fn init(&self) -> std::io::Result<()> { tokio::fs::create_dir_all(self.base_path()).await?; Ok(()) @@ -56,7 +56,7 @@ trait StorageDisk { tokio::fs::write(path, data).await } /// Store data at a path calculated from the hash of the data. Uses content-addressable storage with 2 levels - async fn store_hashed(&self, relative_path: &str, data: &[u8]) -> std::io::Result<()> { + async fn store_hashed(&self, relative_path: &str, data: &[u8]) -> std::io::Result { self.store_hashed_with_extension(relative_path, data, None) .await } @@ -67,7 +67,7 @@ trait StorageDisk { relative_path: &str, data: &[u8], extension: Option<&str>, - ) -> std::io::Result<()> { + ) -> std::io::Result { let hash = sha256::digest(data); let hashed_path = format!( @@ -80,7 +80,8 @@ trait StorageDisk { ext.trim_start_matches('.') )) ); - self.store(&hashed_path, data).await + self.store(&hashed_path, data).await?; + Ok(hashed_path) } async fn read(&self, relative_path: &str) -> std::io::Result> { match tokio::fs::read(self.path(relative_path)).await { From 6b699328150b6bfffa25b6add05a036fef6a63f0 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 15 Apr 2026 00:08:29 +0300 Subject: [PATCH 10/69] fix: what --- src/endpoints/mod_version_submissions.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index e2633ec5..0da17d66 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -53,8 +53,6 @@ struct CommentsQuery { per_page: Option, } -/// Resolve a mod-version's numeric id from its string version tag, and -/// return both it and the verified-mod id. async fn resolve_version_id( mod_id: &str, version: &str, From 1f7f6fc73cd078eaf526ad1a38658c56286b5899 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 15 Apr 2026 00:28:03 +0300 Subject: [PATCH 11/69] feat: add audit tables for comments and submissions --- ...fc578f6b28b7713207b17310ba98ce8fb4004.json | 29 ++++++ ...1d99a5825489bd6b2279c7aaa7f52687221c7.json | 14 +++ ...fa22a00f19dca10f573b3f9331a830b0884d7.json | 28 ++++++ ...16d4e4b91d9ae80380a3191a14e49c629bf7d.json | 29 ++++++ ...reate_mod_version_submissions_table.up.sql | 31 ++++++ ...eate_submission_comment_attachments.up.sql | 15 ++- .../repository/mod_version_submissions.rs | 80 ++++++++++++++-- src/endpoints/mod_version_submissions.rs | 96 +++++++++++++++---- src/endpoints/mod_versions.rs | 15 ++- src/types/models/audit_actions.rs | 26 +++++ src/types/models/mod.rs | 1 + 11 files changed, 327 insertions(+), 37 deletions(-) create mode 100644 .sqlx/query-08d95a8c03eb7561a59d21554abfc578f6b28b7713207b17310ba98ce8fb4004.json create mode 100644 .sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json create mode 100644 .sqlx/query-b248e326b0e2d3c803bedffef28fa22a00f19dca10f573b3f9331a830b0884d7.json create mode 100644 .sqlx/query-f3f632c6f1aa43789e299dc3a9f16d4e4b91d9ae80380a3191a14e49c629bf7d.json create mode 100644 src/types/models/audit_actions.rs diff --git a/.sqlx/query-08d95a8c03eb7561a59d21554abfc578f6b28b7713207b17310ba98ce8fb4004.json b/.sqlx/query-08d95a8c03eb7561a59d21554abfc578f6b28b7713207b17310ba98ce8fb4004.json new file mode 100644 index 00000000..e701d96a --- /dev/null +++ b/.sqlx/query-08d95a8c03eb7561a59d21554abfc578f6b28b7713207b17310ba98ce8fb4004.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_version_submission_comment_audit (comment_id, action, details, performed_by)VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + { + "Custom": { + "name": "audit_action", + "kind": { + "Enum": [ + "created", + "updated", + "deleted", + "restored" + ] + } + } + }, + "Text", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "08d95a8c03eb7561a59d21554abfc578f6b28b7713207b17310ba98ce8fb4004" +} diff --git a/.sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json b/.sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json new file mode 100644 index 00000000..f216c198 --- /dev/null +++ b/.sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM mod_version_submission_comment_attachments WHERE comment_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7" +} diff --git a/.sqlx/query-b248e326b0e2d3c803bedffef28fa22a00f19dca10f573b3f9331a830b0884d7.json b/.sqlx/query-b248e326b0e2d3c803bedffef28fa22a00f19dca10f573b3f9331a830b0884d7.json new file mode 100644 index 00000000..ff275ba1 --- /dev/null +++ b/.sqlx/query-b248e326b0e2d3c803bedffef28fa22a00f19dca10f573b3f9331a830b0884d7.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n filename, COUNT(*) as count\n FROM mod_version_submission_comment_attachments\n WHERE filename = ANY($1)\n GROUP BY filename", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "filename", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "TextArray" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "b248e326b0e2d3c803bedffef28fa22a00f19dca10f573b3f9331a830b0884d7" +} diff --git a/.sqlx/query-f3f632c6f1aa43789e299dc3a9f16d4e4b91d9ae80380a3191a14e49c629bf7d.json b/.sqlx/query-f3f632c6f1aa43789e299dc3a9f16d4e4b91d9ae80380a3191a14e49c629bf7d.json new file mode 100644 index 00000000..dee34896 --- /dev/null +++ b/.sqlx/query-f3f632c6f1aa43789e299dc3a9f16d4e4b91d9ae80380a3191a14e49c629bf7d.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_version_submissions_audit (submission_id, action, details, performed_by)\n VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + { + "Custom": { + "name": "audit_action", + "kind": { + "Enum": [ + "created", + "updated", + "deleted", + "restored" + ] + } + } + }, + "Text", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "f3f632c6f1aa43789e299dc3a9f16d4e4b91d9ae80380a3191a14e49c629bf7d" +} diff --git a/migrations/20260310175835_create_mod_version_submissions_table.up.sql b/migrations/20260310175835_create_mod_version_submissions_table.up.sql index 6e442084..fd658d25 100644 --- a/migrations/20260310175835_create_mod_version_submissions_table.up.sql +++ b/migrations/20260310175835_create_mod_version_submissions_table.up.sql @@ -1,5 +1,12 @@ -- Add up migration script here +CREATE TYPE audit_action AS ENUM ( + 'created', + 'updated', + 'deleted', + 'restored' +); + CREATE TABLE mod_version_submissions ( mod_version_id INT NOT NULL PRIMARY KEY, locked BOOLEAN NOT NULL DEFAULT FALSE, @@ -10,6 +17,19 @@ CREATE TABLE mod_version_submissions ( FOREIGN KEY (locked_by) REFERENCES developers(id) ON DELETE SET NULL ); +CREATE TABLE mod_version_submissions_audit ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + submission_id INT NOT NULL, + action audit_action NOT NULL, + details TEXT, + performed_by INT, + performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + FOREIGN KEY (submission_id) REFERENCES mod_version_submissions(mod_version_id) ON DELETE CASCADE, + FOREIGN KEY (performed_by) REFERENCES developers(id) ON DELETE SET NULL +); + +CREATE INDEX idx_submissions_audit_submission_id ON mod_version_submissions_audit(submission_id); + CREATE TABLE mod_version_submission_comments ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, submission_id INT NOT NULL, @@ -23,3 +43,14 @@ CREATE TABLE mod_version_submission_comments ( CREATE INDEX idx_submission_comments_submission_id ON mod_version_submission_comments(submission_id); + +CREATE TABLE mod_version_submission_comment_audit ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + comment_id BIGINT NOT NULL, + action audit_action NOT NULL, + details TEXT, + performed_by INT, + performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + FOREIGN KEY (comment_id) REFERENCES mod_version_submission_comments(id) ON DELETE CASCADE, + FOREIGN KEY (performed_by) REFERENCES developers(id) ON DELETE SET NULL +); diff --git a/migrations/20260312205413_create_submission_comment_attachments.up.sql b/migrations/20260312205413_create_submission_comment_attachments.up.sql index 7dfc769f..c7528ca4 100644 --- a/migrations/20260312205413_create_submission_comment_attachments.up.sql +++ b/migrations/20260312205413_create_submission_comment_attachments.up.sql @@ -1,16 +1,13 @@ -- Add up migration script here -CREATE TABLE mod_version_submission_comment_attachments -( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - comment_id BIGINT NOT NULL, - filename TEXT NOT NULL, -- SHA-256 hex of WebP content; file on disk: {STORAGE_PATH}/submission-attachments/{filename} +CREATE TABLE mod_version_submission_comment_attachments ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + comment_id BIGINT NOT NULL, + filename TEXT NOT NULL, -- SHA-256 hex of WebP content; file on disk: {STORAGE_PATH}/submission-attachments/{filename} created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (comment_id) REFERENCES mod_version_submission_comments (id) ON DELETE CASCADE ); -CREATE INDEX idx_submission_attachments_comment_id - ON mod_version_submission_comment_attachments (comment_id); +CREATE INDEX idx_submission_attachments_comment_id ON mod_version_submission_comment_attachments (comment_id); -- For fast COUNT lookups on delete (deduplication check) -CREATE INDEX idx_submission_attachments_filename - ON mod_version_submission_comment_attachments (filename); +CREATE INDEX idx_submission_attachments_filename ON mod_version_submission_comment_attachments (filename); diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index a1a119c0..f2bba3b1 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -1,4 +1,5 @@ use crate::database::DatabaseError; +use crate::types::models::audit_actions::AuditAction; use crate::types::models::mod_version_submission::{ ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionRow, }; @@ -28,17 +29,19 @@ pub async fn create( mod_version_id: i32, conn: &mut PgConnection, ) -> Result { - sqlx::query_as!( + let row = sqlx::query_as!( ModVersionSubmissionRow, "INSERT INTO mod_version_submissions (mod_version_id) VALUES ($1) RETURNING mod_version_id, locked, locked_by, created_at, updated_at", mod_version_id ) - .fetch_one(conn) + .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::create failed: {e}")) - .map_err(|e| e.into()) + .inspect_err(|e| log::error!("mod_version_submissions::create failed: {e}"))?; + + insert_submission_audit(mod_version_id, AuditAction::Created, None, None, conn).await?; + Ok(row) } pub async fn set_locked( @@ -47,6 +50,23 @@ pub async fn set_locked( locked_by: Option, conn: &mut PgConnection, ) -> Result { + insert_submission_audit( + mod_version_id, + AuditAction::Updated, + Some(&format!( + "Submission {}{}", + if locked { "locked" } else { "unlocked" }, + if locked_by.is_none() { + "automatically" + } else { + "" + } + )), + locked_by, + &mut *conn, + ) + .await?; + sqlx::query_as!( ModVersionSubmissionRow, "UPDATE mod_version_submissions @@ -274,9 +294,11 @@ pub async fn delete_attachments_for_comment( "DELETE FROM mod_version_submission_comment_attachments WHERE comment_id = $1", comment_id ) - .execute(conn) - .await - .inspect_err(|e| log::error!("mod_version_submissions::delete_attachments_for_comment_id failed: {e}"))?; + .execute(conn) + .await + .inspect_err(|e| { + log::error!("mod_version_submissions::delete_attachments_for_comment_id failed: {e}") + })?; Ok(result.rows_affected() > 0) } @@ -321,3 +343,47 @@ pub async fn count_references_to_filenames( }) .map_err(|e| e.into()) } + +pub async fn insert_submission_audit( + id: i32, + action: AuditAction, + details: Option<&str>, + performed_by: Option, + conn: &mut PgConnection, +) -> Result<(), DatabaseError> { + sqlx::query!( + "INSERT INTO mod_version_submissions_audit (submission_id, action, details, performed_by) + VALUES ($1, $2, $3, $4)", + id, + action as AuditAction, + details, + performed_by + ) + .execute(conn) + .await + .map(|_| ()) + .inspect_err(|e| log::error!("mod_version_submissions::insert_submission_audit failed: {e}")) + .map_err(|e| e.into()) +} + +pub async fn insert_comment_audit( + id: i64, + action: AuditAction, + details: Option<&str>, + performed_by: Option, + conn: &mut PgConnection, +) -> Result<(), DatabaseError> { + sqlx::query!( + "INSERT INTO mod_version_submission_comment_audit (comment_id, action, details, performed_by)\ + VALUES ($1, $2, $3, $4)", + id, + action as AuditAction, + details, + performed_by + ) + .execute(conn) + .await + .map(|_| ()) + .inspect_err(|e| log::error!("mod_version_submissions::insert_comment_audit failed: {e}")) + .map_err(|e| e.into()) +} diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 0da17d66..09692295 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -4,6 +4,7 @@ use crate::database::repository::{developers, mod_version_submissions, mod_versi use crate::extractors::auth::Auth; use crate::storage::StorageDisk; use crate::types::api::{ApiResponse, PaginatedData}; +use crate::types::models::audit_actions::AuditAction; use crate::types::models::mod_version_submission::{ CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, ModVersionSubmissionComment, UpdateCommentPayload, UpdateSubmissionPayload, @@ -138,22 +139,26 @@ pub async fn update_submission( auth.check_admin()?; let mut pool = data.db().acquire().await?; - if !mods::exists(&path.id, &mut pool).await? { + let mut tx = pool.begin().await?; + + if !mods::exists(&path.id, &mut tx).await? { return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + let version_id = resolve_version_id(&path.id, &path.version, &mut tx).await?; - mod_version_submissions::get_for_mod_version(version_id, &mut pool) + mod_version_submissions::get_for_mod_version(version_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; let locked_by_id = if payload.locked { Some(dev.id) } else { None }; let row = - mod_version_submissions::set_locked(version_id, payload.locked, locked_by_id, &mut pool) + mod_version_submissions::set_locked(version_id, payload.locked, locked_by_id, &mut tx) .await?; + tx.commit().await?; + let locked_by = if payload.locked { Some(dev.clone()) } else { @@ -278,14 +283,15 @@ pub async fn create_comment( } let mut pool = data.db().acquire().await?; + let mut tx = pool.begin().await?; - if !mods::exists(&path.id, &mut pool).await? { + if !mods::exists(&path.id, &mut tx).await? { return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + let version_id = resolve_version_id(&path.id, &path.version, &mut tx).await?; - let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; @@ -296,12 +302,23 @@ pub async fn create_comment( } // Only the mod developers (or admins) may comment - if !dev.admin && !developers::has_access_to_mod(dev.id, &path.id, &mut pool).await? { + if !dev.admin && !developers::has_access_to_mod(dev.id, &path.id, &mut tx).await? { return Err(ApiError::Authorization); } - let row = mod_version_submissions::create_comment(version_id, dev.id, &comment_text, &mut pool) - .await?; + let row = + mod_version_submissions::create_comment(version_id, dev.id, &comment_text, &mut tx).await?; + + mod_version_submissions::insert_comment_audit( + row.id, + AuditAction::Created, + None, + Some(dev.id), + &mut tx, + ) + .await?; + + tx.commit().await?; Ok(HttpResponse::Created().json(ApiResponse { error: "".into(), @@ -345,14 +362,15 @@ pub async fn update_comment( } let mut pool = data.db().acquire().await?; + let mut tx = pool.begin().await?; - if !mods::exists(&path.id, &mut pool).await? { + if !mods::exists(&path.id, &mut tx).await? { return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + let version_id = resolve_version_id(&path.id, &path.version, &mut tx).await?; - let submission = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + let submission = mod_version_submissions::get_for_mod_version(version_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; @@ -362,7 +380,7 @@ pub async fn update_comment( )); } - let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut pool) + let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) .await? .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; @@ -377,10 +395,22 @@ pub async fn update_comment( return Err(ApiError::Authorization); } + mod_version_submissions::insert_comment_audit( + comment_row.id, + AuditAction::Updated, + Some(&format!( + "Updated comment. Previously: {}", + comment_row.comment + )), + Some(dev.id), + &mut tx, + ) + .await?; + let updated_row = - mod_version_submissions::update_comment(path.comment_id, &comment_text, &mut pool).await?; + mod_version_submissions::update_comment(path.comment_id, &comment_text, &mut tx).await?; - let author = developers::get_one(updated_row.author_id, &mut pool) + let author = developers::get_one(updated_row.author_id, &mut tx) .await? .ok_or_else(|| ApiError::InternalError("Author not found".into()))?; @@ -454,6 +484,15 @@ pub async fn delete_comment( .map(|a| a.filename) .collect::>(); + mod_version_submissions::insert_comment_audit( + comment_row.id, + AuditAction::Deleted, + None, + Some(dev.id), + &mut tx, + ) + .await?; + mod_version_submissions::delete_comment(path.comment_id, &mut tx).await?; let references = @@ -657,6 +696,23 @@ pub async fn upload_attachments( result.push(row.into_attachment(&data.app_url().to_string())); } + mod_version_submissions::insert_comment_audit( + comment_row.id, + AuditAction::Updated, + Some(&format!( + "Attached {} file{}", + result.len(), + if result.len() > 1 || result.is_empty() { + "s" + } else { + "" + } + )), + Some(dev.id), + &mut tx, + ) + .await?; + tx.commit().await?; Ok(HttpResponse::Created().json(ApiResponse { @@ -739,6 +795,14 @@ pub async fn delete_attachment( let filename = attachment.filename.clone(); mod_version_submissions::delete_attachment(path.attachment_id, &mut tx).await?; + mod_version_submissions::insert_comment_audit( + comment_row.id, + AuditAction::Updated, + Some("Removed an attachment"), + Some(dev.id), + &mut tx, + ) + .await?; let remaining = mod_version_submissions::count_references_to_filename(&filename, &mut tx).await?; diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 285d2f23..5906d818 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -1,9 +1,9 @@ use std::str::FromStr; -use actix_web::{dev::ConnectionInfo, get, post, put, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, dev::ConnectionInfo, get, post, put, web}; use serde::Deserialize; -use sqlx::{types::ipnetwork::IpNetwork, Acquire}; -use utoipa::{ToSchema, IntoParams}; +use sqlx::{Acquire, types::ipnetwork::IpNetwork}; +use utoipa::{IntoParams, ToSchema}; use crate::config::AppData; use crate::database::repository::{ @@ -21,7 +21,7 @@ use crate::{ extractors::auth::Auth, types::{ api::ApiResponse, - mod_json::{split_version_and_compare, ModJson}, + mod_json::{ModJson, split_version_and_compare}, models::{ mod_gd_version::{GDVersionEnum, VerPlatform}, mod_version::{self, ModVersion}, @@ -437,7 +437,6 @@ pub async fn create_version( mods::update_with_json_moved(the_mod, json, &mut tx).await?; } - if !make_accepted { mod_version_submissions::create(version.id, &mut tx).await?; } @@ -577,6 +576,12 @@ pub async fn update_version( mod_tags::update_for_mod(&the_mod.id, &tags, &mut tx).await?; mods::update_with_json_moved(the_mod, json, &mut tx).await?; + + // Let's also maybe lock the thread if it exists! + let thread = mod_version_submissions::get_for_mod_version(version.id, &mut tx).await?; + if let Some(thread) = thread && !thread.locked { + mod_version_submissions::set_locked(thread.mod_version_id, true, None, &mut tx).await?; + } } tx.commit().await?; diff --git a/src/types/models/audit_actions.rs b/src/types/models/audit_actions.rs new file mode 100644 index 00000000..e6e7bdc5 --- /dev/null +++ b/src/types/models/audit_actions.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use utoipa::ToSchema; + +#[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, ToSchema)] +#[sqlx(type_name = "audit_action", rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum AuditAction { + Created, + Updated, + Deleted, + Restored, +} + +impl FromStr for AuditAction { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "created" => Ok(AuditAction::Created), + "updated" => Ok(AuditAction::Updated), + "deleted" => Ok(AuditAction::Deleted), + "restored" => Ok(AuditAction::Restored), + _ => Err(()), + } + } +} diff --git a/src/types/models/mod.rs b/src/types/models/mod.rs index ac1abd90..3e3e996b 100644 --- a/src/types/models/mod.rs +++ b/src/types/models/mod.rs @@ -13,3 +13,4 @@ pub mod mod_version_status; pub mod mod_version_submission; pub mod stats; pub mod tag; +pub mod audit_actions; From 88c09c03d3b68fc7869d8f07d3cf05f414e9a37f Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 15 Apr 2026 02:07:43 +0300 Subject: [PATCH 12/69] fix(submissions): allow index admins to comment on locked threads --- src/endpoints/mod_version_submissions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 09692295..7e5c89a4 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -295,7 +295,7 @@ pub async fn create_comment( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if submission.locked { + if !dev.admin && submission.locked { return Err(ApiError::BadRequest( "Submission is locked; no new comments allowed".into(), )); @@ -374,7 +374,7 @@ pub async fn update_comment( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if submission.locked { + if !dev.admin && submission.locked { return Err(ApiError::BadRequest( "Submission is locked; comments cannot be edited".into(), )); @@ -608,7 +608,7 @@ pub async fn upload_attachments( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if submission.locked { + if !dev.admin && submission.locked { return Err(ApiError::BadRequest( "Submission is locked; attachments cannot be uploaded".into(), )); From 42dce49f9efeb1d7733bad3a7f2b2bbd41b399bb Mon Sep 17 00:00:00 2001 From: Fleeym Date: Tue, 21 Apr 2026 21:38:13 +0300 Subject: [PATCH 13/69] feat: expose comment & submission audit --- .../repository/mod_version_submissions.rs | 38 +++++++- src/endpoints/mod_version_submissions.rs | 92 ++++++++++++++++++- src/main.rs | 2 + src/types/models/audit_actions.rs | 9 ++ 4 files changed, 139 insertions(+), 2 deletions(-) diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index f2bba3b1..bfb760ea 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -1,5 +1,5 @@ use crate::database::DatabaseError; -use crate::types::models::audit_actions::AuditAction; +use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::mod_version_submission::{ ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionRow, }; @@ -25,6 +25,24 @@ pub async fn get_for_mod_version( .map_err(|e| e.into()) } +pub async fn get_audit_for_submission( + id: i32, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!( + AuditActionRow, + r#"SELECT + action as "action: _", details, performed_by, performed_at + FROM mod_version_submissions_audit + WHERE submission_id = $1"#, + id + ) + .fetch_all(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::get_audit_for_submission failed: {e}",)) + .map_err(|e| e.into()) +} + pub async fn create( mod_version_id: i32, conn: &mut PgConnection, @@ -199,6 +217,24 @@ pub async fn delete_comment( Ok(result.rows_affected() > 0) } +pub async fn get_audit_for_comment( + id: i64, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!( + AuditActionRow, + r#"SELECT + action as "action: _", details, performed_by, performed_at + FROM mod_version_submission_comment_audit + WHERE comment_id = $1"#, + id + ) + .fetch_all(conn) + .await + .inspect_err(|e| log::error!("mod_version_submissions::get_audit_for_comment failed: {e}",)) + .map_err(|e| e.into()) +} + pub async fn count_attachments_for_comment( comment_id: i64, conn: &mut PgConnection, diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 7e5c89a4..bc91157f 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -4,7 +4,7 @@ use crate::database::repository::{developers, mod_version_submissions, mod_versi use crate::extractors::auth::Auth; use crate::storage::StorageDisk; use crate::types::api::{ApiResponse, PaginatedData}; -use crate::types::models::audit_actions::AuditAction; +use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::mod_version_submission::{ CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, ModVersionSubmissionComment, UpdateCommentPayload, UpdateSubmissionPayload, @@ -113,6 +113,47 @@ pub async fn get_submission( })) } +#[utoipa::path( + path = "/v1/mods/{id}/versions/{version}/submission/audit", + tag = "mod_version_submissions", + params(SubmissionPath), + responses( + (status = 200, description = "Submission audit", body = inline(ApiResponse>)), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden - admin only"), + (status = 404, description = "Mod, version, or submission not found"), + ), + security(("bearer_token" = [])) +)] +#[get("v1/mods/{id}/versions/{version}/submission/audit")] +pub async fn get_submission_audit( + path: web::Path, + data: web::Data, + auth: Auth, +) -> Result { + auth.check_admin()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + let row = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + let audit = + mod_version_submissions::get_audit_for_submission(row.mod_version_id, &mut pool).await?; + + Ok(web::Json(ApiResponse { + error: "".into(), + payload: audit, + })) +} + /// Update (lock / unlock) a submission (admin only) #[utoipa::path( put, @@ -248,6 +289,55 @@ pub async fn get_comments( })) } +#[utoipa::path( + path = "/v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/audit", + tag = "mod_version_submissions", + params(CommentPath), + responses( + (status = 200, description = "Comment audit", body = inline(ApiResponse>)), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden - admin only"), + (status = 404, description = "Mod, version, submission or comment not found"), + ), + security(("bearer_token" = [])) +)] +#[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/audit")] +pub async fn get_comment_audit( + path: web::Path, + data: web::Data, + auth: Auth, +) -> Result { + auth.check_admin()?; + + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); + } + + let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + + let row = mod_version_submissions::get_for_mod_version(version_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + + let comment = mod_version_submissions::get_comment(path.comment_id, &mut pool) + .await? + .ok_or_else(|| ApiError::NotFound("Comment not found".into()))?; + + if comment.submission_id != row.mod_version_id { + return Err(ApiError::NotFound("Comment not found".into())); + } + + let audit = + mod_version_submissions::get_audit_for_comment(comment.id, &mut pool).await?; + + Ok(web::Json(ApiResponse { + error: "".into(), + payload: audit, + })) +} + /// Add a comment to a mod version submission #[utoipa::path( post, diff --git a/src/main.rs b/src/main.rs index 07972e27..80308a06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,8 +74,10 @@ async fn main() -> anyhow::Result<()> { .service(endpoints::mod_versions::create_version) .service(endpoints::mod_versions::update_version) .service(endpoints::mod_version_submissions::get_submission) + .service(endpoints::mod_version_submissions::get_submission_audit) .service(endpoints::mod_version_submissions::update_submission) .service(endpoints::mod_version_submissions::get_comments) + .service(endpoints::mod_version_submissions::get_comment_audit) .service(endpoints::mod_version_submissions::create_comment) .service(endpoints::mod_version_submissions::update_comment) .service(endpoints::mod_version_submissions::delete_comment) diff --git a/src/types/models/audit_actions.rs b/src/types/models/audit_actions.rs index e6e7bdc5..0bad8c16 100644 --- a/src/types/models/audit_actions.rs +++ b/src/types/models/audit_actions.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use utoipa::ToSchema; @@ -24,3 +25,11 @@ impl FromStr for AuditAction { } } } + +#[derive(Serialize, Debug, Clone, ToSchema)] +pub struct AuditActionRow { + pub action: AuditAction, + pub details: Option, + pub performed_by: Option, + pub performed_at: DateTime, +} From 40e85692ab083f22e0bafa0060ac5ea19d2f28be Mon Sep 17 00:00:00 2001 From: Fleeym Date: Tue, 21 Apr 2026 21:45:16 +0300 Subject: [PATCH 14/69] fix: simplify if let --- src/endpoints/mod_version_submissions.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index bc91157f..48bafc31 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -592,10 +592,8 @@ pub async fn delete_comment( tx.commit().await?; for (filename, count) in references { - if count == 0 { - if let Err(e) = data.static_storage().delete(&filename).await { - error!("Failed to delete attachment file {}: {e}", filename); - } + if count == 0 && let Err(e) = data.static_storage().delete(&filename).await { + log::error!("Failed to delete attachment file {filename}: {e}"); } } From c07dac593a89572b6a408f7e15698a234ce11406 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 22 Apr 2026 22:37:27 +0300 Subject: [PATCH 15/69] feat(threads): rework locking system Allowed any mod developer to comment if they have at least 1 accepted mod on the index. Also introduced 3 locking levels: - No lock - Locked to mod developers and admins - Locked --- ...a387a44c47030bcac9aba859ed5080dcc165e.json | 70 ++++++++++++++++ ...bd71dcb15053d899b6a8cd6cc75327d52bff.json} | 19 ++++- ...c8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json | 48 ----------- ...47ba12f948e60e815ad917846d7ff6733245a.json | 52 ++++++++++++ ...4e806b378409a148cb0728c03c70470954f5.json} | 19 ++++- ...11a909895fdc0e98541bed9816362862968f7.json | 52 ++++++++++++ ...1fcc9f5ebe72d4948e0403ef5d488a9c8a370.json | 22 +++++ ...ate_mod_version_submissions_table.down.sql | 6 +- ...reate_mod_version_submissions_table.up.sql | 8 +- src/database/repository/developers.rs | 20 +++++ .../repository/mod_version_submissions.rs | 30 ++++--- src/endpoints/mod_version_submissions.rs | 82 +++++++++++-------- src/endpoints/mod_versions.rs | 23 ++++-- src/types/models/mod_version_submission.rs | 17 +++- 14 files changed, 354 insertions(+), 114 deletions(-) create mode 100644 .sqlx/query-3a9e0f2e4c810e63811251c2da5a387a44c47030bcac9aba859ed5080dcc165e.json rename .sqlx/{query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json => query-5687bf51d606c491a75a05ffed72bd71dcb15053d899b6a8cd6cc75327d52bff.json} (55%) delete mode 100644 .sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json create mode 100644 .sqlx/query-6be04be9bbf53814144d17355ba47ba12f948e60e815ad917846d7ff6733245a.json rename .sqlx/{query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json => query-9a23bc60988017bdb2d3770f0cbe4e806b378409a148cb0728c03c70470954f5.json} (60%) create mode 100644 .sqlx/query-dfe205a13013db7ee5dbeb0ea3011a909895fdc0e98541bed9816362862968f7.json create mode 100644 .sqlx/query-f2c57d8bc7752c080fc45a6dd5b1fcc9f5ebe72d4948e0403ef5d488a9c8a370.json diff --git a/.sqlx/query-3a9e0f2e4c810e63811251c2da5a387a44c47030bcac9aba859ed5080dcc165e.json b/.sqlx/query-3a9e0f2e4c810e63811251c2da5a387a44c47030bcac9aba859ed5080dcc165e.json new file mode 100644 index 00000000..04a238dd --- /dev/null +++ b/.sqlx/query-3a9e0f2e4c810e63811251c2da5a387a44c47030bcac9aba859ed5080dcc165e.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE mod_version_submissions\n SET lock = $1, locked_by = $2, updated_at = NOW()\n WHERE mod_version_id = $3\n RETURNING mod_version_id, lock as \"lock: _\", locked_by, created_at, updated_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_version_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "lock: _", + "type_info": { + "Custom": { + "name": "submission_lock", + "kind": { + "Enum": [ + "none", + "internal", + "locked" + ] + } + } + } + }, + { + "ordinal": 2, + "name": "locked_by", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + { + "Custom": { + "name": "submission_lock", + "kind": { + "Enum": [ + "none", + "internal", + "locked" + ] + } + } + }, + "Int4", + "Int4" + ] + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "3a9e0f2e4c810e63811251c2da5a387a44c47030bcac9aba859ed5080dcc165e" +} diff --git a/.sqlx/query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json b/.sqlx/query-5687bf51d606c491a75a05ffed72bd71dcb15053d899b6a8cd6cc75327d52bff.json similarity index 55% rename from .sqlx/query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json rename to .sqlx/query-5687bf51d606c491a75a05ffed72bd71dcb15053d899b6a8cd6cc75327d52bff.json index 7ed59aa9..606c83e6 100644 --- a/.sqlx/query-120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9.json +++ b/.sqlx/query-5687bf51d606c491a75a05ffed72bd71dcb15053d899b6a8cd6cc75327d52bff.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n mod_version_id, locked, locked_by,\n created_at, updated_at\n FROM mod_version_submissions\n WHERE mod_version_id = $1", + "query": "SELECT\n mod_version_id, lock as \"lock: _\", locked_by,\n created_at, updated_at\n FROM mod_version_submissions\n WHERE mod_version_id = $1", "describe": { "columns": [ { @@ -10,8 +10,19 @@ }, { "ordinal": 1, - "name": "locked", - "type_info": "Bool" + "name": "lock: _", + "type_info": { + "Custom": { + "name": "submission_lock", + "kind": { + "Enum": [ + "none", + "internal", + "locked" + ] + } + } + } }, { "ordinal": 2, @@ -42,5 +53,5 @@ false ] }, - "hash": "120ac6102a436b096d442dc5e9c0af4628f2b5803a59b2c28c6052250d60bef9" + "hash": "5687bf51d606c491a75a05ffed72bd71dcb15053d899b6a8cd6cc75327d52bff" } diff --git a/.sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json b/.sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json deleted file mode 100644 index 4c06ced0..00000000 --- a/.sqlx/query-66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE mod_version_submissions\n SET locked = $1, locked_by = $2, updated_at = NOW()\n WHERE mod_version_id = $3\n RETURNING mod_version_id, locked, locked_by, created_at, updated_at", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_version_id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "locked", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "locked_by", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "updated_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Bool", - "Int4", - "Int4" - ] - }, - "nullable": [ - false, - false, - true, - false, - false - ] - }, - "hash": "66da5cd3367182b9d560fe1f1fac8ef8aa6dd3425c5c609b6e0e2aa7cbdb2b0c" -} diff --git a/.sqlx/query-6be04be9bbf53814144d17355ba47ba12f948e60e815ad917846d7ff6733245a.json b/.sqlx/query-6be04be9bbf53814144d17355ba47ba12f948e60e815ad917846d7ff6733245a.json new file mode 100644 index 00000000..3df40f25 --- /dev/null +++ b/.sqlx/query-6be04be9bbf53814144d17355ba47ba12f948e60e815ad917846d7ff6733245a.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n action as \"action: _\", details, performed_by, performed_at\n FROM mod_version_submissions_audit\n WHERE submission_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "action: _", + "type_info": { + "Custom": { + "name": "audit_action", + "kind": { + "Enum": [ + "created", + "updated", + "deleted", + "restored" + ] + } + } + } + }, + { + "ordinal": 1, + "name": "details", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "performed_by", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "performed_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + true, + true, + false + ] + }, + "hash": "6be04be9bbf53814144d17355ba47ba12f948e60e815ad917846d7ff6733245a" +} diff --git a/.sqlx/query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json b/.sqlx/query-9a23bc60988017bdb2d3770f0cbe4e806b378409a148cb0728c03c70470954f5.json similarity index 60% rename from .sqlx/query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json rename to .sqlx/query-9a23bc60988017bdb2d3770f0cbe4e806b378409a148cb0728c03c70470954f5.json index 9a6bda4d..05d0e991 100644 --- a/.sqlx/query-bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443.json +++ b/.sqlx/query-9a23bc60988017bdb2d3770f0cbe4e806b378409a148cb0728c03c70470954f5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO mod_version_submissions (mod_version_id)\n VALUES ($1)\n RETURNING mod_version_id, locked, locked_by, created_at, updated_at", + "query": "INSERT INTO mod_version_submissions (mod_version_id)\n VALUES ($1)\n RETURNING mod_version_id, lock as \"lock: _\", locked_by, created_at, updated_at", "describe": { "columns": [ { @@ -10,8 +10,19 @@ }, { "ordinal": 1, - "name": "locked", - "type_info": "Bool" + "name": "lock: _", + "type_info": { + "Custom": { + "name": "submission_lock", + "kind": { + "Enum": [ + "none", + "internal", + "locked" + ] + } + } + } }, { "ordinal": 2, @@ -42,5 +53,5 @@ false ] }, - "hash": "bc0b5bdd2259f360b41ff48d7fc7bc3b731d8ecc313548af8f9afcd523fa6443" + "hash": "9a23bc60988017bdb2d3770f0cbe4e806b378409a148cb0728c03c70470954f5" } diff --git a/.sqlx/query-dfe205a13013db7ee5dbeb0ea3011a909895fdc0e98541bed9816362862968f7.json b/.sqlx/query-dfe205a13013db7ee5dbeb0ea3011a909895fdc0e98541bed9816362862968f7.json new file mode 100644 index 00000000..a2e157fb --- /dev/null +++ b/.sqlx/query-dfe205a13013db7ee5dbeb0ea3011a909895fdc0e98541bed9816362862968f7.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n action as \"action: _\", details, performed_by, performed_at\n FROM mod_version_submission_comment_audit\n WHERE comment_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "action: _", + "type_info": { + "Custom": { + "name": "audit_action", + "kind": { + "Enum": [ + "created", + "updated", + "deleted", + "restored" + ] + } + } + } + }, + { + "ordinal": 1, + "name": "details", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "performed_by", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "performed_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + true, + false + ] + }, + "hash": "dfe205a13013db7ee5dbeb0ea3011a909895fdc0e98541bed9816362862968f7" +} diff --git a/.sqlx/query-f2c57d8bc7752c080fc45a6dd5b1fcc9f5ebe72d4948e0403ef5d488a9c8a370.json b/.sqlx/query-f2c57d8bc7752c080fc45a6dd5b1fcc9f5ebe72d4948e0403ef5d488a9c8a370.json new file mode 100644 index 00000000..b232b577 --- /dev/null +++ b/.sqlx/query-f2c57d8bc7752c080fc45a6dd5b1fcc9f5ebe72d4948e0403ef5d488a9c8a370.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT mods.id FROM mods\n INNER JOIN mod_versions ON mods.id = mod_versions.mod_id\n INNER JOIN mod_version_statuses ON mod_version_statuses.id = mod_versions.status_id\n INNER JOIN mods_developers ON mods.id = mods_developers.mod_id\n WHERE mods_developers.developer_id = $1\n AND mod_version_statuses.status = 'accepted'\n LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f2c57d8bc7752c080fc45a6dd5b1fcc9f5ebe72d4948e0403ef5d488a9c8a370" +} diff --git a/migrations/20260310175835_create_mod_version_submissions_table.down.sql b/migrations/20260310175835_create_mod_version_submissions_table.down.sql index 81adb2d9..51dfa79c 100644 --- a/migrations/20260310175835_create_mod_version_submissions_table.down.sql +++ b/migrations/20260310175835_create_mod_version_submissions_table.down.sql @@ -1,4 +1,8 @@ -- Add down migration script here +DROP TABLE IF EXISTS mod_version_submission_comment_audit; DROP TABLE IF EXISTS mod_version_submission_comments; -DROP TABLE IF EXISTS mod_version_submissions; \ No newline at end of file +DROP TABLE IF EXISTS mod_version_submissions_audit; +DROP TABLE IF EXISTS mod_version_submissions; +DROP TYPE IF EXISTS audit_action; +DROP TYPE IF EXISTS submission_lock; \ No newline at end of file diff --git a/migrations/20260310175835_create_mod_version_submissions_table.up.sql b/migrations/20260310175835_create_mod_version_submissions_table.up.sql index fd658d25..08bf499d 100644 --- a/migrations/20260310175835_create_mod_version_submissions_table.up.sql +++ b/migrations/20260310175835_create_mod_version_submissions_table.up.sql @@ -7,9 +7,15 @@ CREATE TYPE audit_action AS ENUM ( 'restored' ); +CREATE TYPE submission_lock AS ENUM ( + 'none', + 'internal', + 'locked' +); + CREATE TABLE mod_version_submissions ( mod_version_id INT NOT NULL PRIMARY KEY, - locked BOOLEAN NOT NULL DEFAULT FALSE, + lock submission_lock NOT NULL DEFAULT 'none', locked_by INT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index c8037e0d..dc761915 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -293,6 +293,26 @@ pub async fn has_access_to_mod( .map_err(|e| e.into()) } +pub async fn has_active_mod( + dev_id: i32, + conn: &mut PgConnection +) -> Result { + sqlx::query!( + "SELECT mods.id FROM mods + INNER JOIN mod_versions ON mods.id = mod_versions.mod_id + INNER JOIN mod_version_statuses ON mod_version_statuses.id = mod_versions.status_id + INNER JOIN mods_developers ON mods.id = mods_developers.mod_id + WHERE mods_developers.developer_id = $1 + AND mod_version_statuses.status = 'accepted' + LIMIT 1", + dev_id + ).fetch_optional(conn) + .await + .inspect_err(|e| log::error!("developers::has_active_mod failed: {e}")) + .map_err(|e| e.into()) + .map(|result| result.is_some()) +} + pub async fn owns_mod( dev_id: i32, mod_id: &str, diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index bfb760ea..f642c3b4 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -1,7 +1,7 @@ use crate::database::DatabaseError; use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::mod_version_submission::{ - ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionRow, + ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionLock, ModVersionSubmissionRow }; use sqlx::{Error, PgConnection}; use std::collections::HashMap; @@ -12,11 +12,11 @@ pub async fn get_for_mod_version( ) -> Result, DatabaseError> { sqlx::query_as!( ModVersionSubmissionRow, - "SELECT - mod_version_id, locked, locked_by, + r#"SELECT + mod_version_id, lock as "lock: _", locked_by, created_at, updated_at FROM mod_version_submissions - WHERE mod_version_id = $1", + WHERE mod_version_id = $1"#, id ) .fetch_optional(conn) @@ -49,9 +49,9 @@ pub async fn create( ) -> Result { let row = sqlx::query_as!( ModVersionSubmissionRow, - "INSERT INTO mod_version_submissions (mod_version_id) + r#"INSERT INTO mod_version_submissions (mod_version_id) VALUES ($1) - RETURNING mod_version_id, locked, locked_by, created_at, updated_at", + RETURNING mod_version_id, lock as "lock: _", locked_by, created_at, updated_at"#, mod_version_id ) .fetch_one(&mut *conn) @@ -64,7 +64,7 @@ pub async fn create( pub async fn set_locked( mod_version_id: i32, - locked: bool, + lock: ModVersionSubmissionLock, locked_by: Option, conn: &mut PgConnection, ) -> Result { @@ -73,9 +73,13 @@ pub async fn set_locked( AuditAction::Updated, Some(&format!( "Submission {}{}", - if locked { "locked" } else { "unlocked" }, + match lock { + ModVersionSubmissionLock::None => "unlocked", + ModVersionSubmissionLock::Internal => "restricted to mod developers and admins", + ModVersionSubmissionLock::Locked => "locked" + }, if locked_by.is_none() { - "automatically" + " automatically" } else { "" } @@ -87,11 +91,11 @@ pub async fn set_locked( sqlx::query_as!( ModVersionSubmissionRow, - "UPDATE mod_version_submissions - SET locked = $1, locked_by = $2, updated_at = NOW() + r#"UPDATE mod_version_submissions + SET lock = $1, locked_by = $2, updated_at = NOW() WHERE mod_version_id = $3 - RETURNING mod_version_id, locked, locked_by, created_at, updated_at", - locked, + RETURNING mod_version_id, lock as "lock: _", locked_by, created_at, updated_at"#, + lock as ModVersionSubmissionLock, locked_by, mod_version_id ) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 48bafc31..b0697f26 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -1,13 +1,16 @@ use super::ApiError; use crate::config::AppData; +use crate::database::DatabaseError; use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; use crate::extractors::auth::Auth; use crate::storage::StorageDisk; use crate::types::api::{ApiResponse, PaginatedData}; use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; +use crate::types::models::developer::Developer; use crate::types::models::mod_version_submission::{ CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, - ModVersionSubmissionComment, UpdateCommentPayload, UpdateSubmissionPayload, + ModVersionSubmissionComment, ModVersionSubmissionLock, UpdateCommentPayload, + UpdateSubmissionPayload, }; use actix_multipart::Multipart; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; @@ -65,6 +68,26 @@ async fn resolve_version_id( Ok(ver.id) } +async fn check_submission_lock( + dev: &Developer, + mod_id: &str, + lock_status: ModVersionSubmissionLock, + conn: &mut PgConnection, +) -> Result { + if dev.admin { + return Ok(true); + } + + let access_to_mod = developers::has_access_to_mod(dev.id, mod_id, &mut *conn).await?; + let active_developer = developers::has_active_mod(dev.id, &mut *conn).await?; + + Ok(match lock_status { + ModVersionSubmissionLock::None => access_to_mod || active_developer, + ModVersionSubmissionLock::Internal => access_to_mod, + ModVersionSubmissionLock::Locked => false, + }) +} + /// Get the submission for a mod version #[utoipa::path( get, @@ -192,18 +215,20 @@ pub async fn update_submission( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - let locked_by_id = if payload.locked { Some(dev.id) } else { None }; + let locked_by_id = if payload.lock != ModVersionSubmissionLock::None { + Some(dev.id) + } else { + None + }; - let row = - mod_version_submissions::set_locked(version_id, payload.locked, locked_by_id, &mut tx) - .await?; + let row = mod_version_submissions::set_locked(version_id, payload.lock, locked_by_id, &mut tx) + .await?; tx.commit().await?; - let locked_by = if payload.locked { - Some(dev.clone()) - } else { - None + let locked_by = match payload.lock { + ModVersionSubmissionLock::None => None, + _ => Some(dev.clone()), }; Ok(web::Json(ApiResponse { @@ -329,8 +354,7 @@ pub async fn get_comment_audit( return Err(ApiError::NotFound("Comment not found".into())); } - let audit = - mod_version_submissions::get_audit_for_comment(comment.id, &mut pool).await?; + let audit = mod_version_submissions::get_audit_for_comment(comment.id, &mut pool).await?; Ok(web::Json(ApiResponse { error: "".into(), @@ -385,7 +409,7 @@ pub async fn create_comment( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if !dev.admin && submission.locked { + if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { return Err(ApiError::BadRequest( "Submission is locked; no new comments allowed".into(), )); @@ -464,7 +488,7 @@ pub async fn update_comment( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if !dev.admin && submission.locked { + if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { return Err(ApiError::BadRequest( "Submission is locked; comments cannot be edited".into(), )); @@ -481,7 +505,7 @@ pub async fn update_comment( ))); } - if !dev.admin && comment_row.author_id != dev.id { + if comment_row.author_id != dev.id { return Err(ApiError::Authorization); } @@ -546,7 +570,7 @@ pub async fn delete_comment( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if submission.locked && !dev.admin { + if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { return Err(ApiError::BadRequest( "Submission is locked; comments cannot be deleted".into(), )); @@ -557,10 +581,7 @@ pub async fn delete_comment( .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; if comment_row.submission_id != version_id { - return Err(ApiError::NotFound(format!( - "Comment {} does not belong to this submission", - path.comment_id - ))); + return Err(ApiError::NotFound("Comment not found".to_string())); } if !dev.admin && comment_row.author_id != dev.id { @@ -592,7 +613,9 @@ pub async fn delete_comment( tx.commit().await?; for (filename, count) in references { - if count == 0 && let Err(e) = data.static_storage().delete(&filename).await { + if count == 0 + && let Err(e) = data.static_storage().delete(&filename).await + { log::error!("Failed to delete attachment file {filename}: {e}"); } } @@ -696,7 +719,7 @@ pub async fn upload_attachments( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if !dev.admin && submission.locked { + if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { return Err(ApiError::BadRequest( "Submission is locked; attachments cannot be uploaded".into(), )); @@ -707,10 +730,7 @@ pub async fn upload_attachments( .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; if comment_row.submission_id != version_id { - return Err(ApiError::NotFound(format!( - "Comment {} does not belong to this submission", - path.comment_id - ))); + return Err(ApiError::NotFound("Comment not found".into())); } if comment_row.author_id != dev.id { @@ -752,7 +772,7 @@ pub async fn upload_attachments( ))); } - // Decode → encode WebP → hash, all in a blocking thread + // Decode -> encode WebP -> hash, all in a blocking thread let processed: Vec> = tokio::task::spawn_blocking(move || -> Result>, ApiError> { images @@ -771,7 +791,8 @@ pub async fn upload_attachments( .collect() }) .await - .map_err(|e| ApiError::InternalError(format!("Task join error: {e}")))??; + .inspect_err(|e| log::error!("Bad bad bad bad {e}")) + .map_err(|_| ApiError::InternalError("Something very bad happened!".into()))??; let mut result = Vec::with_capacity(processed.len()); for webp_bytes in &processed { @@ -847,7 +868,7 @@ pub async fn delete_attachment( .await? .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; - if submission.locked && !dev.admin { + if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { return Err(ApiError::BadRequest( "Submission is locked; attachments cannot be deleted".into(), )); @@ -858,10 +879,7 @@ pub async fn delete_attachment( .ok_or_else(|| ApiError::NotFound(format!("Comment {} not found", path.comment_id)))?; if comment_row.submission_id != version_id { - return Err(ApiError::NotFound(format!( - "Comment {} does not belong to this submission", - path.comment_id - ))); + return Err(ApiError::NotFound("Comment not found".into())); } if !dev.admin && comment_row.author_id != dev.id { diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 5906d818..f4f6958e 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -16,6 +16,7 @@ use crate::events::mod_created::{ }; use crate::mod_zip::{self, download_mod}; use crate::types::models; +use crate::types::models::mod_version_submission::ModVersionSubmissionLock; use crate::webhook::discord::DiscordWebhook; use crate::{ extractors::auth::Auth, @@ -427,11 +428,11 @@ pub async fn create_version( ) .await?; } - if let Some(tags) = &json.tags { - if !tags.is_empty() { - let tags = models::tag::parse_tag_list(tags, &the_mod.id, &mut tx).await?; - mod_tags::update_for_mod(&the_mod.id, &tags, &mut tx).await?; - } + if let Some(tags) = &json.tags + && !tags.is_empty() + { + let tags = models::tag::parse_tag_list(tags, &the_mod.id, &mut tx).await?; + mod_tags::update_for_mod(&the_mod.id, &tags, &mut tx).await?; } mods::update_with_json_moved(the_mod, json, &mut tx).await?; @@ -579,8 +580,16 @@ pub async fn update_version( // Let's also maybe lock the thread if it exists! let thread = mod_version_submissions::get_for_mod_version(version.id, &mut tx).await?; - if let Some(thread) = thread && !thread.locked { - mod_version_submissions::set_locked(thread.mod_version_id, true, None, &mut tx).await?; + if let Some(thread) = thread + && thread.lock != ModVersionSubmissionLock::Locked + { + mod_version_submissions::set_locked( + thread.mod_version_id, + ModVersionSubmissionLock::Locked, + None, + &mut tx, + ) + .await?; } } diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index b06e5d0d..a8171260 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -3,10 +3,19 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use crate::types::models::developer::Developer; +#[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, ToSchema)] +#[sqlx(type_name = "submission_lock", rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum ModVersionSubmissionLock { + None, + Internal, + Locked +} + #[derive(Serialize, ToSchema, Debug, Clone)] pub struct ModVersionSubmission { pub mod_version_id: i32, - pub locked: bool, + pub lock: ModVersionSubmissionLock, pub locked_by: Option, pub created_at: DateTime, pub updated_at: DateTime, @@ -14,7 +23,7 @@ pub struct ModVersionSubmission { pub struct ModVersionSubmissionRow { pub mod_version_id: i32, - pub locked: bool, + pub lock: ModVersionSubmissionLock, pub locked_by: Option, pub created_at: DateTime, pub updated_at: DateTime, @@ -24,7 +33,7 @@ impl ModVersionSubmissionRow { pub fn into_submission(self, locked_by: Option) -> ModVersionSubmission { ModVersionSubmission { mod_version_id: self.mod_version_id, - locked: self.locked, + lock: self.lock, locked_by, created_at: self.created_at, updated_at: self.updated_at, @@ -95,7 +104,7 @@ impl ModVersionSubmissionAttachmentRow { #[derive(Deserialize, ToSchema)] pub struct UpdateSubmissionPayload { - pub locked: bool, + pub lock: ModVersionSubmissionLock, } #[derive(Deserialize, ToSchema)] From 26edc7214305b1edaf5fe5ab9c60458967efc8a8 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 22 Apr 2026 22:43:17 +0300 Subject: [PATCH 16/69] fix(threads): fix unnecessary string copy --- src/endpoints/mod_version_submissions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index b0697f26..a1267b0e 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -802,7 +802,7 @@ pub async fn upload_attachments( .await?; let row = mod_version_submissions::create_attachment(path.comment_id, &filename, &mut tx).await?; - result.push(row.into_attachment(&data.app_url().to_string())); + result.push(row.into_attachment(data.app_url())); } mod_version_submissions::insert_comment_audit( From 8916a69d7b85f82a3f99d743b46242419f2c9c9c Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 22 Apr 2026 23:27:49 +0300 Subject: [PATCH 17/69] feat(threads): add admin discord webhook --- src/config.rs | 7 ++++ .../repository/mod_version_submissions.rs | 16 --------- src/endpoints/mod_version_submissions.rs | 35 +++++++++---------- src/events/mod.rs | 3 +- src/events/thread_comment.rs | 23 ++++++++++++ 5 files changed, 48 insertions(+), 36 deletions(-) create mode 100644 src/events/thread_comment.rs diff --git a/src/config.rs b/src/config.rs index 5b9ca5d7..6eaedcb1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,7 @@ pub struct AppData { front_url: String, github: GitHubClientData, webhook_url: String, + index_admin_webhook_url: String, static_storage: StaticStorage, private_storage: PrivateStorage, disable_downloads: bool, @@ -51,6 +52,7 @@ pub async fn build_config() -> anyhow::Result { let github_client = dotenvy::var("GITHUB_CLIENT_ID").unwrap_or("".to_string()); let github_secret = dotenvy::var("GITHUB_CLIENT_SECRET").unwrap_or("".to_string()); let webhook_url = dotenvy::var("DISCORD_WEBHOOK_URL").unwrap_or("".to_string()); + let index_admin_webhook_url = dotenvy::var("INDEX_ADMIN_DISCORD_WEBHOOK_URL").unwrap_or("".to_string()); let disable_downloads = dotenvy::var("DISABLE_DOWNLOAD_COUNTS").unwrap_or("0".to_string()) == "1"; let max_download_mb = dotenvy::var("MAX_MOD_FILESIZE_MB") @@ -72,6 +74,7 @@ pub async fn build_config() -> anyhow::Result { client_secret: github_secret, }, webhook_url, + index_admin_webhook_url, static_storage: StaticStorage::new(), private_storage: PrivateStorage::new(), disable_downloads, @@ -113,6 +116,10 @@ impl AppData { &self.webhook_url } + pub fn index_admin_webhook_url(&self) -> &str { + &self.index_admin_webhook_url + } + pub fn disable_downloads(&self) -> bool { self.disable_downloads } diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index f642c3b4..87361dd3 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -326,22 +326,6 @@ pub async fn delete_attachment( Ok(result.rows_affected() > 0) } -pub async fn delete_attachments_for_comment( - comment_id: i64, - conn: &mut PgConnection, -) -> Result { - let result = sqlx::query!( - "DELETE FROM mod_version_submission_comment_attachments WHERE comment_id = $1", - comment_id - ) - .execute(conn) - .await - .inspect_err(|e| { - log::error!("mod_version_submissions::delete_attachments_for_comment_id failed: {e}") - })?; - Ok(result.rows_affected() > 0) -} - pub async fn count_references_to_filename( filename: &str, conn: &mut PgConnection, diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index a1267b0e..958bbda0 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -2,6 +2,7 @@ use super::ApiError; use crate::config::AppData; use crate::database::DatabaseError; use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; +use crate::events::thread_comment::NewThreadComment; use crate::extractors::auth::Auth; use crate::storage::StorageDisk; use crate::types::api::{ApiResponse, PaginatedData}; @@ -12,6 +13,7 @@ use crate::types::models::mod_version_submission::{ ModVersionSubmissionComment, ModVersionSubmissionLock, UpdateCommentPayload, UpdateSubmissionPayload, }; +use crate::webhook::discord::DiscordWebhook; use actix_multipart::Multipart; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use futures::StreamExt; @@ -410,13 +412,6 @@ pub async fn create_comment( .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { - return Err(ApiError::BadRequest( - "Submission is locked; no new comments allowed".into(), - )); - } - - // Only the mod developers (or admins) may comment - if !dev.admin && !developers::has_access_to_mod(dev.id, &path.id, &mut tx).await? { return Err(ApiError::Authorization); } @@ -434,6 +429,16 @@ pub async fn create_comment( tx.commit().await?; + if !data.index_admin_webhook_url().is_empty() { + NewThreadComment { + mod_id: path.id.clone(), + mod_version: path.version.clone(), + username: dev.username.clone(), + } + .to_discord_webhook() + .send(data.index_admin_webhook_url()); + } + Ok(HttpResponse::Created().json(ApiResponse { error: "".into(), payload: row.into_comment(dev), @@ -489,9 +494,7 @@ pub async fn update_comment( .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { - return Err(ApiError::BadRequest( - "Submission is locked; comments cannot be edited".into(), - )); + return Err(ApiError::Authorization); } let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) @@ -571,9 +574,7 @@ pub async fn delete_comment( .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { - return Err(ApiError::BadRequest( - "Submission is locked; comments cannot be deleted".into(), - )); + return Err(ApiError::Authorization); } let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) @@ -720,9 +721,7 @@ pub async fn upload_attachments( .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { - return Err(ApiError::BadRequest( - "Submission is locked; attachments cannot be uploaded".into(), - )); + return Err(ApiError::Authorization); } let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) @@ -869,9 +868,7 @@ pub async fn delete_attachment( .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; if !check_submission_lock(&dev, &path.id, submission.lock, &mut tx).await? { - return Err(ApiError::BadRequest( - "Submission is locked; attachments cannot be deleted".into(), - )); + return Err(ApiError::Authorization); } let comment_row = mod_version_submissions::get_comment(path.comment_id, &mut tx) diff --git a/src/events/mod.rs b/src/events/mod.rs index 745b0ae0..f77d7bfe 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -1,2 +1,3 @@ pub mod mod_created; -pub mod mod_feature; \ No newline at end of file +pub mod mod_feature; +pub mod thread_comment; \ No newline at end of file diff --git a/src/events/thread_comment.rs b/src/events/thread_comment.rs new file mode 100644 index 00000000..22b5ec15 --- /dev/null +++ b/src/events/thread_comment.rs @@ -0,0 +1,23 @@ +use crate::webhook::discord::{DiscordMessage, DiscordWebhook}; + +pub struct NewThreadComment { + pub mod_id: String, + pub mod_version: String, + pub username: String, +} + +impl DiscordWebhook for NewThreadComment { + fn to_discord_webhook(&self) -> DiscordMessage { + DiscordMessage::new().embed( + &format!( + "✅ New comment on thread {} v{}", + self.mod_id, self.mod_version, + ), + Some(&format!( + "https://geode-sdk.org/mods/{}?version={}\n\nComment posted by {}", + self.mod_id, self.mod_version, self.username + )), + None, + ) + } +} From bc645811d2b5328b1a1a64611ad0f6e815749f4f Mon Sep 17 00:00:00 2001 From: Fleeym Date: Wed, 22 Apr 2026 23:41:02 +0300 Subject: [PATCH 18/69] feat(threads): add new unverified version create event --- src/endpoints/mod_versions.rs | 15 ++++++++++++--- src/endpoints/mods.rs | 18 ++++++++++++++---- src/events/mod_created.rs | 25 ++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index f4f6958e..ea32eff0 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -12,7 +12,7 @@ use crate::database::repository::{ }; use crate::endpoints::ApiError; use crate::events::mod_created::{ - NewModAcceptedEvent, NewModVersionAcceptedEvent, NewModVersionVerification, + NewModAcceptedEvent, NewModVersionAcceptedEvent, NewModVersionVerification, NewUnverifiedModVersionCreated, }; use crate::mod_zip::{self, download_mod}; use crate::types::models; @@ -303,7 +303,7 @@ pub async fn create_version( let id = path.into_inner(); - let the_mod = mods::get_one(&id, false, &mut pool) + let mut the_mod = mods::get_one(&id, false, &mut pool) .await? .ok_or(ApiError::NotFound(format!("Mod {} not found", &id)))?; @@ -435,7 +435,7 @@ pub async fn create_version( mod_tags::update_for_mod(&the_mod.id, &tags, &mut tx).await?; } - mods::update_with_json_moved(the_mod, json, &mut tx).await?; + the_mod = mods::update_with_json_moved(the_mod, json, &mut tx).await?; } if !make_accepted { @@ -459,6 +459,15 @@ pub async fn create_version( } .to_discord_webhook() .send(data.webhook_url()); + } else { + NewUnverifiedModVersionCreated { + id: the_mod.id.clone(), + name: version.name.clone(), + version: version.name.clone(), + owner: dev.clone(), + } + .to_discord_webhook() + .send(data.index_admin_webhook_url()); } version.modify_metadata(data.app_url(), false); diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index 7050c4cb..42d1110d 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -8,6 +8,7 @@ use crate::database::repository::mod_versions; use crate::database::repository::mods; use crate::database::repository::{dependencies, deprecations, mod_version_submissions}; use crate::endpoints::ApiError; +use crate::events::mod_created::NewUnverifiedModVersionCreated; use crate::events::mod_feature::ModFeaturedEvent; use crate::extractors::auth::Auth; use crate::mod_zip; @@ -24,7 +25,7 @@ use actix_web::{HttpResponse, Responder, get, post, put, web}; use serde::Deserialize; use serde::Serialize; use sqlx::Acquire; -use utoipa::{ToSchema, IntoParams}; +use utoipa::{IntoParams, ToSchema}; #[derive(Deserialize, Default, Hash, Eq, PartialEq, ToSchema)] #[serde(rename_all = "snake_case")] @@ -74,7 +75,6 @@ pub struct CreateQueryParams { (status = 403, description = "Forbidden") ) )] - #[get("/v1/mods")] pub async fn index( data: web::Data, @@ -208,7 +208,9 @@ pub async fn create( let existing: Option = mods::get_one(&json.id, false, &mut pool).await?; if json.id.starts_with("geode.") && !dev.admin { - return Err(ApiError::BadRequest("Only index admins may use mod ids that start with 'geode.'".into())); + return Err(ApiError::BadRequest( + "Only index admins may use mod ids that start with 'geode.'".into(), + )); } if let Some(m) = &existing { @@ -271,13 +273,21 @@ pub async fn create( the_mod.developers = developers::get_all_for_mod(&the_mod.id, &mut tx).await?; the_mod.versions.insert(0, version); - // First version is always pending, so always open a submission for review let first_version = the_mod.versions.first().unwrap(); mod_version_submissions::create(first_version.id, &mut tx).await?; tx.commit().await?; + NewUnverifiedModVersionCreated { + id: the_mod.id.clone(), + name: first_version.name.clone(), + version: first_version.name.clone(), + owner: dev.clone(), + } + .to_discord_webhook() + .send(data.index_admin_webhook_url()); + for i in &mut the_mod.versions { i.modify_metadata(data.app_url(), false); } diff --git a/src/events/mod_created.rs b/src/events/mod_created.rs index 530b9327..16ff89ac 100644 --- a/src/events/mod_created.rs +++ b/src/events/mod_created.rs @@ -1,6 +1,13 @@ use crate::types::models::developer::Developer; use crate::webhook::discord::{DiscordMessage, DiscordWebhook}; +pub struct NewUnverifiedModVersionCreated { + pub id: String, + pub name: String, + pub version: String, + pub owner: Developer, +} + pub struct NewModAcceptedEvent { pub id: String, pub name: String, @@ -51,7 +58,23 @@ impl DiscordWebhook for NewModVersionAcceptedEvent { "https://geode-sdk.org/mods/{}\n\nOwned by [{}](https://github.com/{})\n{}", self.id, self.owner.display_name, self.owner.username, accepted_msg )), - Some(&format!("{}/v1/mods/{}/logo?version={}", self.base_url, self.id, self.version)), + Some(&format!( + "{}/v1/mods/{}/logo?version={}", + self.base_url, self.id, self.version + )), + ) + } +} + +impl DiscordWebhook for NewUnverifiedModVersionCreated { + fn to_discord_webhook(&self) -> DiscordMessage { + DiscordMessage::new().embed( + &format!("✅ New mod version pending: {} {}", self.name, self.version), + Some(&format!( + "https://geode-sdk.org/mods/{}?version={}\n\nOwned by [{}](https://github.com/{})", + self.id, self.version, self.owner.display_name, self.owner.username + )), + None, ) } } From b3d79103fc658be7565dc16ba99b79b5b58caeef Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 23 Apr 2026 00:59:53 +0300 Subject: [PATCH 19/69] fix: fix ApiError::NotFound status code --- src/endpoints/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 91d24806..d41e0417 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -63,6 +63,7 @@ impl actix_web::ResponseError for ApiError { ApiError::Authorization => StatusCode::FORBIDDEN, ApiError::Json(..) => StatusCode::BAD_REQUEST, ApiError::TooManyRequests(..) => StatusCode::TOO_MANY_REQUESTS, + ApiError::NotFound(..) => StatusCode::NOT_FOUND, _ => StatusCode::INTERNAL_SERVER_ERROR, } } From 988152fb276e9c464e374c7679608c0df06cc981 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 23 Apr 2026 01:06:40 +0300 Subject: [PATCH 20/69] fix: fix lockfile --- Cargo.lock | 181 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 122 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b588c7e..ab5ad6eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,9 +36,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.12.0" +version = "3.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662" dependencies = [ "actix-codec", "actix-rt", @@ -64,8 +64,8 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.9.4", - "sha1", + "rand 0.10.1", + "sha1 0.11.0", "smallvec", "tokio", "tokio-util", @@ -100,7 +100,7 @@ dependencies = [ "log", "memchr", "mime", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "serde_plain", @@ -525,9 +525,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -535,9 +535,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -590,6 +590,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "brotli" version = "8.0.2" @@ -726,15 +735,15 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -754,9 +763,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -833,6 +842,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -982,6 +997,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.35.0" @@ -1052,7 +1076,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -1125,12 +1149,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1649,7 +1684,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1734,6 +1769,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.9.0" @@ -2150,9 +2194,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" [[package]] name = "libc" @@ -2356,7 +2400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2490,7 +2534,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -2633,7 +2677,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", + "digest 0.10.7", "hmac", ] @@ -2679,7 +2723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2860,9 +2904,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "qoi" @@ -2958,9 +3002,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -3219,8 +3263,8 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -3297,9 +3341,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ "aws-lc-rs", "once_cell", @@ -3360,9 +3404,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.12" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -3530,7 +3574,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -3541,7 +3596,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -3579,7 +3634,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -3759,7 +3814,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -3776,10 +3831,10 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", - "sha1", + "sha1 0.10.6", "sha2", "smallvec", "sqlx-core", @@ -3817,7 +3872,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sha2", @@ -4094,9 +4149,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -4259,9 +4314,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" @@ -4342,15 +4397,17 @@ dependencies = [ ] [[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4408,9 +4465,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -4499,11 +4556,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -4512,7 +4569,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -4644,9 +4701,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -5054,6 +5111,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -5296,7 +5359,7 @@ dependencies = [ "memchr", "pbkdf2", "ppmd-rust", - "sha1", + "sha1 0.10.6", "time", "typed-path", "zeroize", From 353fbe447bf481535f02dd02e7e5f36a8d20f921 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 23 Apr 2026 01:20:40 +0300 Subject: [PATCH 21/69] feat(threads): include attachments in comment payload --- .../repository/mod_version_submissions.rs | 32 ++++++++++++++- src/endpoints/mod_version_submissions.rs | 41 +++++++++++++++---- src/types/models/mod_version_submission.rs | 4 +- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index 87361dd3..b180625b 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -1,7 +1,8 @@ use crate::database::DatabaseError; use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::mod_version_submission::{ - ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionLock, ModVersionSubmissionRow + ModVersionSubmissionAttachmentRow, ModVersionSubmissionCommentRow, ModVersionSubmissionLock, + ModVersionSubmissionRow, }; use sqlx::{Error, PgConnection}; use std::collections::HashMap; @@ -76,7 +77,7 @@ pub async fn set_locked( match lock { ModVersionSubmissionLock::None => "unlocked", ModVersionSubmissionLock::Internal => "restricted to mod developers and admins", - ModVersionSubmissionLock::Locked => "locked" + ModVersionSubmissionLock::Locked => "locked", }, if locked_by.is_none() { " automatically" @@ -295,6 +296,33 @@ pub async fn get_attachments_for_comment( .map_err(|e: Error| e.into()) } +pub async fn get_attachments_for_comments( + comment_ids: &[i64], + conn: &mut PgConnection, +) -> Result>, DatabaseError> { + let rows = sqlx::query_as!( + ModVersionSubmissionAttachmentRow, + "SELECT id, comment_id, filename, created_at + FROM mod_version_submission_comment_attachments + WHERE comment_id = ANY($1) + ORDER BY id ASC", + comment_ids + ) + .fetch_all(conn) + .await + .inspect_err(|e| { + log::error!("mod_version_submissions::get_attachments_for_comments failed: {e}") + })?; + + let mut ret: HashMap> = HashMap::with_capacity(comment_ids.len()); + + for row in rows { + ret.entry(row.comment_id).or_default().push(row); + } + + Ok(ret) +} + pub async fn get_attachment( attachment_id: i64, conn: &mut PgConnection, diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 958bbda0..a635d3de 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -106,11 +106,8 @@ async fn check_submission_lock( #[get("v1/mods/{id}/versions/{version}/submission")] pub async fn get_submission( path: web::Path, - data: web::Data, - auth: Auth, + data: web::Data ) -> Result { - auth.developer()?; - let mut pool = data.db().acquire().await?; if !mods::exists(&path.id, &mut pool).await? { @@ -290,12 +287,27 @@ pub async fn get_comments( ids }; + let comment_ids = rows.iter().map(|i| i.id).collect::>(); + let authors_map = developers::get_many_by_id(&author_ids, &mut pool) .await? .into_iter() .map(|dev| (dev.id, dev)) .collect::>(); + let mut attachments_map = + mod_version_submissions::get_attachments_for_comments(&comment_ids, &mut pool) + .await? + .into_iter() + .map(|(k, v)| { + let transformed = v + .into_iter() + .map(|i| data.public_storage().asset_url(&i.filename)) + .collect(); + (k, transformed) + }) + .collect::>>(); + let comments = rows .into_iter() .map(|row| { @@ -303,7 +315,13 @@ pub async fn get_comments( .get(&row.author_id) .cloned() .ok_or_else(|| ApiError::InternalError("Author not found".into()))?; - Ok(row.into_comment(author)) + + let id = row.id; + + Ok(row.into_comment( + author, + attachments_map.remove(&id).unwrap_or_default() + )) }) .collect::, ApiError>>()?; @@ -441,7 +459,7 @@ pub async fn create_comment( Ok(HttpResponse::Created().json(ApiResponse { error: "".into(), - payload: row.into_comment(dev), + payload: row.into_comment(dev, vec![]), })) } @@ -531,9 +549,18 @@ pub async fn update_comment( .await? .ok_or_else(|| ApiError::InternalError("Author not found".into()))?; + let attachments = + mod_version_submissions::get_attachments_for_comment(path.comment_id, &mut tx) + .await? + .into_iter() + .map(|i| data.public_storage().asset_url(&i.filename)) + .collect::>(); + + tx.commit().await?; + Ok(web::Json(ApiResponse { error: "".into(), - payload: updated_row.into_comment(author), + payload: updated_row.into_comment(author, attachments), })) } diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index a8171260..721fda32 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -47,6 +47,7 @@ pub struct ModVersionSubmissionComment { pub submission_id: i32, pub comment: String, pub author: Developer, + pub attachments: Vec, pub created_at: DateTime, pub updated_at: Option>, } @@ -61,12 +62,13 @@ pub struct ModVersionSubmissionCommentRow { } impl ModVersionSubmissionCommentRow { - pub fn into_comment(self, author: Developer) -> ModVersionSubmissionComment { + pub fn into_comment(self, author: Developer, attachment_links: Vec) -> ModVersionSubmissionComment { ModVersionSubmissionComment { id: self.id, submission_id: self.submission_id, comment: self.comment, author, + attachments: attachment_links, created_at: self.created_at, updated_at: self.updated_at, } From cf952dcfab429ef8ddcb85231eef5bb43ee7eee4 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 23 Apr 2026 01:28:28 +0300 Subject: [PATCH 22/69] fix(threads): don't require auth for comments --- src/endpoints/mod_version_submissions.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index a635d3de..508a145a 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -253,11 +253,8 @@ pub async fn update_submission( pub async fn get_comments( path: web::Path, data: web::Data, - query: web::Query, - auth: Auth, + query: web::Query ) -> Result { - auth.developer()?; - let mut pool = data.db().acquire().await?; if !mods::exists(&path.id, &mut pool).await? { @@ -667,11 +664,8 @@ pub async fn delete_comment( #[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] pub async fn get_attachments( path: web::Path, - data: web::Data, - auth: Auth, + data: web::Data ) -> Result { - auth.developer()?; - let mut pool = data.db().acquire().await?; if !mods::exists(&path.id, &mut pool).await? { From e48c86df609d7a288f68ff474c8dd2ef628f2d38 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 26 Apr 2026 01:20:10 +0300 Subject: [PATCH 23/69] fix: use chrono_dt_secs for new DateTImes --- src/types/models/audit_actions.rs | 2 ++ src/types/models/mod_version_submission.rs | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/types/models/audit_actions.rs b/src/types/models/audit_actions.rs index 0bad8c16..02e77e3f 100644 --- a/src/types/models/audit_actions.rs +++ b/src/types/models/audit_actions.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use utoipa::ToSchema; +use crate::types::serde::chrono_dt_secs; #[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, ToSchema)] #[sqlx(type_name = "audit_action", rename_all = "lowercase")] @@ -31,5 +32,6 @@ pub struct AuditActionRow { pub action: AuditAction, pub details: Option, pub performed_by: Option, + #[serde(with = "chrono_dt_secs")] pub performed_at: DateTime, } diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index 721fda32..2b42b831 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -1,7 +1,8 @@ +use crate::types::models::developer::Developer; +use crate::types::serde::chrono_dt_secs; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::types::models::developer::Developer; #[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, ToSchema)] #[sqlx(type_name = "submission_lock", rename_all = "lowercase")] @@ -9,7 +10,7 @@ use crate::types::models::developer::Developer; pub enum ModVersionSubmissionLock { None, Internal, - Locked + Locked, } #[derive(Serialize, ToSchema, Debug, Clone)] @@ -48,7 +49,9 @@ pub struct ModVersionSubmissionComment { pub comment: String, pub author: Developer, pub attachments: Vec, + #[serde(with = "chrono_dt_secs")] pub created_at: DateTime, + #[serde(with = "chrono_dt_secs::option")] pub updated_at: Option>, } @@ -57,18 +60,24 @@ pub struct ModVersionSubmissionCommentRow { pub submission_id: i32, pub comment: String, pub author_id: i32, + #[serde(with = "chrono_dt_secs")] pub created_at: DateTime, + #[serde(with = "chrono_dt_secs::option")] pub updated_at: Option>, } impl ModVersionSubmissionCommentRow { - pub fn into_comment(self, author: Developer, attachment_links: Vec) -> ModVersionSubmissionComment { + pub fn into_comment( + self, + author: Developer, + attachment_links: Vec, + ) -> ModVersionSubmissionComment { ModVersionSubmissionComment { id: self.id, submission_id: self.submission_id, comment: self.comment, author, - attachments: attachment_links, + attachments: attachment_links, created_at: self.created_at, updated_at: self.updated_at, } @@ -122,4 +131,3 @@ pub struct UpdateCommentPayload { /// Plain text comment; HTML tags are stripped pub comment: String, } - From 263c4a0c8bcaa44bac909c78a9b14d0d3b375af0 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 26 Apr 2026 12:41:44 +0300 Subject: [PATCH 24/69] fix: remove chrono_dt_secs from Row struct --- src/types/models/mod_version_submission.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index 2b42b831..d171f59c 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -60,9 +60,7 @@ pub struct ModVersionSubmissionCommentRow { pub submission_id: i32, pub comment: String, pub author_id: i32, - #[serde(with = "chrono_dt_secs")] pub created_at: DateTime, - #[serde(with = "chrono_dt_secs::option")] pub updated_at: Option>, } @@ -89,6 +87,7 @@ pub struct ModVersionSubmissionAttachment { pub id: i64, pub comment_id: i64, pub url: String, + #[serde(with = "chrono_dt_secs")] pub created_at: DateTime, } From df5b9518eb9ab3e8999c07e58f3dc5765415a661 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 26 Apr 2026 12:43:18 +0300 Subject: [PATCH 25/69] fix: forgot one struct --- src/types/models/mod_version_submission.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index d171f59c..6f0a0dc6 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -18,7 +18,9 @@ pub struct ModVersionSubmission { pub mod_version_id: i32, pub lock: ModVersionSubmissionLock, pub locked_by: Option, + #[serde(with = "chrono_dt_secs")] pub created_at: DateTime, + #[serde(with = "chrono_dt_secs")] pub updated_at: DateTime, } From 046f544af2738a2f2f1609653666f811722aed9f Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 26 Apr 2026 14:17:08 +0300 Subject: [PATCH 26/69] add has_accepted_mod to /v1/me --- src/database/repository/developers.rs | 26 +++++++++++++++++++++----- src/endpoints/developers.rs | 11 ++++++++--- src/types/models/developer.rs | 25 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index dc761915..16e759be 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -293,10 +293,7 @@ pub async fn has_access_to_mod( .map_err(|e| e.into()) } -pub async fn has_active_mod( - dev_id: i32, - conn: &mut PgConnection -) -> Result { +pub async fn has_active_mod(dev_id: i32, conn: &mut PgConnection) -> Result { sqlx::query!( "SELECT mods.id FROM mods INNER JOIN mod_versions ON mods.id = mod_versions.mod_id @@ -306,7 +303,8 @@ pub async fn has_active_mod( AND mod_version_statuses.status = 'accepted' LIMIT 1", dev_id - ).fetch_optional(conn) + ) + .fetch_optional(conn) .await .inspect_err(|e| log::error!("developers::has_active_mod failed: {e}")) .map_err(|e| e.into()) @@ -473,3 +471,21 @@ pub async fn find_by_token( .inspect_err(|e| log::error!("{}", e)) .map_err(|e| e.into()) } + +pub async fn has_accepted_mod(id: i32, conn: &mut PgConnection) -> Result { + sqlx::query!( + "SELECT mod_versions.id + FROM mod_versions + INNER JOIN mods ON mods.id = mod_versions.mod_id + INNER JOIN mods_developers ON mods.id = mods_developers.mod_id + INNER JOIN mod_version_statuses ON mod_version_statuses.id = mod_versions.status_id + WHERE mod_version_statuses.status = 'accepted' + AND mods_developers.developer_id = $1", + id + ) + .fetch_optional(conn) + .await + .map(|x| x.is_some()) + .inspect_err(|e| log::error!("developers::has_accepted_mod failed: {e}")) + .map_err(|e| e.into()) +} diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index da80cabb..b884fe75 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -6,6 +6,7 @@ use super::ApiError; use crate::config::AppData; use crate::database::repository::{auth_tokens, developers, mods, refresh_tokens}; use crate::types::api::{ApiResponse, PaginatedData}; +use crate::types::models::developer::SelfDeveloper; use crate::{ extractors::auth::Auth, types::{ @@ -357,7 +358,7 @@ pub async fn get_own_mods( path = "/v1/me", tag = "developers", responses( - (status = 200, description = "Current developer profile", body = inline(ApiResponse)), + (status = 200, description = "Current developer profile", body = inline(ApiResponse)), (status = 401, description = "Unauthorized") ), security( @@ -365,11 +366,15 @@ pub async fn get_own_mods( ) )] #[get("v1/me")] -pub async fn get_me(auth: Auth) -> Result { +pub async fn get_me(auth: Auth, data: web::Data) -> Result { + let mut pool = data.db().acquire().await?; let dev = auth.developer()?; + + let has_accepted_mod = developers::has_accepted_mod(dev.id, &mut pool).await?; + Ok(HttpResponse::Ok().json(ApiResponse { error: "".to_string(), - payload: dev, + payload: dev.to_self_developer(has_accepted_mod), })) } diff --git a/src/types/models/developer.rs b/src/types/models/developer.rs index 29ac26af..ff28717d 100644 --- a/src/types/models/developer.rs +++ b/src/types/models/developer.rs @@ -18,3 +18,28 @@ pub struct Developer { pub admin: bool, pub github_id: i64, } + +#[derive(sqlx::FromRow, Serialize, Clone, Debug, ToSchema)] +pub struct SelfDeveloper { + pub id: i32, + pub username: String, + pub display_name: String, + pub verified: bool, + pub admin: bool, + pub github_id: i64, + pub has_accepted_mod: bool +} + +impl Developer { + pub fn to_self_developer(&self, has_accepted_mod: bool) -> SelfDeveloper { + SelfDeveloper { + id: self.id, + username: self.username.clone(), + display_name: self.display_name.clone(), + verified: self.verified, + admin: self.admin, + github_id: self.github_id, + has_accepted_mod + } + } +} From d6e016c1eab862c9b7cd19ead1fe245ee1e80021 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 26 Apr 2026 23:05:41 +0300 Subject: [PATCH 27/69] chore: sqlx prepare --- ...66304005a571cd8871cc84b2010315c7d9692.json | 22 ++++++++++ ...f54d36ff0819639c15a2e70d32dce65a39803.json | 40 +++++++++++++++++++ ...1d99a5825489bd6b2279c7aaa7f52687221c7.json | 14 ------- 3 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 .sqlx/query-27c67f136d2f37a916b0bfb8d4966304005a571cd8871cc84b2010315c7d9692.json create mode 100644 .sqlx/query-3e911a3e12f4d239c0d1a3185c7f54d36ff0819639c15a2e70d32dce65a39803.json delete mode 100644 .sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json diff --git a/.sqlx/query-27c67f136d2f37a916b0bfb8d4966304005a571cd8871cc84b2010315c7d9692.json b/.sqlx/query-27c67f136d2f37a916b0bfb8d4966304005a571cd8871cc84b2010315c7d9692.json new file mode 100644 index 00000000..a117b30e --- /dev/null +++ b/.sqlx/query-27c67f136d2f37a916b0bfb8d4966304005a571cd8871cc84b2010315c7d9692.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT mod_versions.id\n FROM mod_versions\n INNER JOIN mods ON mods.id = mod_versions.mod_id\n INNER JOIN mods_developers ON mods.id = mods_developers.mod_id\n INNER JOIN mod_version_statuses ON mod_version_statuses.id = mod_versions.status_id\n WHERE mod_version_statuses.status = 'accepted'\n AND mods_developers.developer_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "27c67f136d2f37a916b0bfb8d4966304005a571cd8871cc84b2010315c7d9692" +} diff --git a/.sqlx/query-3e911a3e12f4d239c0d1a3185c7f54d36ff0819639c15a2e70d32dce65a39803.json b/.sqlx/query-3e911a3e12f4d239c0d1a3185c7f54d36ff0819639c15a2e70d32dce65a39803.json new file mode 100644 index 00000000..6ea2de4f --- /dev/null +++ b/.sqlx/query-3e911a3e12f4d239c0d1a3185c7f54d36ff0819639c15a2e70d32dce65a39803.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, comment_id, filename, created_at\n FROM mod_version_submission_comment_attachments\n WHERE comment_id = ANY($1)\n ORDER BY id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "comment_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "filename", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "3e911a3e12f4d239c0d1a3185c7f54d36ff0819639c15a2e70d32dce65a39803" +} diff --git a/.sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json b/.sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json deleted file mode 100644 index f216c198..00000000 --- a/.sqlx/query-886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM mod_version_submission_comment_attachments WHERE comment_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "886e8f068bd6eccf2cc83e89c581d99a5825489bd6b2279c7aaa7f52687221c7" -} From f4054093d91c7f781c84f71155a4618721e13dbe Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 19 Jun 2026 23:25:35 +0300 Subject: [PATCH 28/69] fix(threads): fix admin webhook version --- src/endpoints/mod_versions.rs | 2 +- src/endpoints/mods.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index ea32eff0..4c956357 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -463,7 +463,7 @@ pub async fn create_version( NewUnverifiedModVersionCreated { id: the_mod.id.clone(), name: version.name.clone(), - version: version.name.clone(), + version: version.version.clone(), owner: dev.clone(), } .to_discord_webhook() diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index 09efcab9..875dc710 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -289,7 +289,7 @@ pub async fn create( NewUnverifiedModVersionCreated { id: the_mod.id.clone(), name: first_version.name.clone(), - version: first_version.name.clone(), + version: first_version.version.clone(), owner: dev.clone(), } .to_discord_webhook() From ca6011f22f1620af0d64245cefa771af5825d188 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 19 Jun 2026 23:26:04 +0300 Subject: [PATCH 29/69] chore: add INDEX_ADMIN_DISCORD_WEBHOOK_URL to .env.example --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index a56aa470..1b5853bd 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,7 @@ GITHUB_CLIENT_SECRET= # Discord DISCORD_WEBHOOK_URL= +INDEX_ADMIN_DISCORD_WEBHOOK_URL= # Config From b3c83dcf861a0d80da09292dcbaef01f205b329b Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 19 Jun 2026 23:31:53 +0300 Subject: [PATCH 30/69] fix(threads): fix orphaned attachment file cleanup --- src/endpoints/mod_version_submissions.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 508a145a..ab22179d 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -637,12 +637,11 @@ pub async fn delete_comment( tx.commit().await?; - for (filename, count) in references { - if count == 0 - && let Err(e) = data.static_storage().delete(&filename).await - { - log::error!("Failed to delete attachment file {filename}: {e}"); - } + for filename in attachment_filenames { + if !references.contains_key(&filename) + && let Err(e) = data.public_storage().delete(&filename).await { + log::error!("Failed to delete attachment file {filename}: {e}"); + } } Ok(HttpResponse::NoContent()) From ac76a40151fff3f9ffb485ed6bae13280d6024a3 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 19 Jun 2026 23:32:51 +0300 Subject: [PATCH 31/69] fix(threads): fix upload_attachments reading all uploaded images --- src/endpoints/mod_version_submissions.rs | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index ab22179d..e04a5570 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -756,20 +756,35 @@ pub async fn upload_attachments( return Err(ApiError::Authorization); } + let existing = + mod_version_submissions::count_attachments_for_comment(path.comment_id, &mut tx).await?; + + const MAX_ATTACHMENTS_PER_COMMENT: i64 = 5; + // Collect all `image` fields from the multipart stream const MAX_BYTES: usize = 5 * 1024 * 1024; + let mut count = 0; let mut images: Vec = Vec::new(); while let Some(field) = multipart.next().await { let mut field = field.map_err(|e| ApiError::BadRequest(e.to_string()))?; if field.name() != Some("image") { continue; } + count += 1; + + if existing + count > MAX_ATTACHMENTS_PER_COMMENT { + return Err(ApiError::BadRequest(format!( + "Comment already has {} attachment(s); adding {} would exceed the limit of 5", + existing, + count + ))); + } let mut buf = bytes::BytesMut::new(); while let Some(chunk) = field.next().await { let chunk = chunk.map_err(|e| ApiError::BadRequest(e.to_string()))?; buf.extend_from_slice(&chunk); if buf.len() > MAX_BYTES { - return Err(ApiError::BadRequest("Image exceeds 5 MB limit".into())); + return Err(ApiError::BadRequest("Image exceeds {} MB limit".into())); } } images.push(buf.freeze()); @@ -781,16 +796,6 @@ pub async fn upload_attachments( )); } - let existing = - mod_version_submissions::count_attachments_for_comment(path.comment_id, &mut tx).await?; - if existing + images.len() as i64 > 5 { - return Err(ApiError::BadRequest(format!( - "Comment already has {} attachment(s); adding {} would exceed the limit of 5", - existing, - images.len() - ))); - } - // Decode -> encode WebP -> hash, all in a blocking thread let processed: Vec> = tokio::task::spawn_blocking(move || -> Result>, ApiError> { From 914791969d5c56c676b7f8de34892bf1c1108678 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 19 Jun 2026 23:34:09 +0300 Subject: [PATCH 32/69] fix(threads): ensure file cleanup on upload_attachments rollback --- src/endpoints/mod_version_submissions.rs | 67 ++++++++++++++---------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index e04a5570..1935e42a 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -819,34 +819,45 @@ pub async fn upload_attachments( .map_err(|_| ApiError::InternalError("Something very bad happened!".into()))??; let mut result = Vec::with_capacity(processed.len()); - for webp_bytes in &processed { - let filename = data - .static_storage() - .store_hashed_with_extension("submission-attachments", webp_bytes, Some("webp")) - .await?; - let row = - mod_version_submissions::create_attachment(path.comment_id, &filename, &mut tx).await?; - result.push(row.into_attachment(data.app_url())); - } - - mod_version_submissions::insert_comment_audit( - comment_row.id, - AuditAction::Updated, - Some(&format!( - "Attached {} file{}", - result.len(), - if result.len() > 1 || result.is_empty() { - "s" - } else { - "" - } - )), - Some(dev.id), - &mut tx, - ) - .await?; + let mut stored_filenames: Vec = Vec::with_capacity(processed.len()); + + let outcome: Result<_, ApiError> = async { + for webp_bytes in &processed { + let filename = data + .public_storage() + .store_hashed_with_extension("submission-attachments", webp_bytes, Some("webp")) + .await?; + stored_filenames.push(filename.clone()); + let row = + mod_version_submissions::create_attachment(path.comment_id, &filename, &mut tx).await?; + result.push(row.into_attachment(data.app_url())); + } + mod_version_submissions::insert_comment_audit( + comment_row.id, + AuditAction::Updated, + Some(&format!( + "Attached {} file{}", + result.len(), + if result.len() > 1 || result.is_empty() { + "s" + } else { + "" + } + )), + Some(dev.id), + &mut tx, + ) + .await?; + tx.commit().await?; + Ok(()) + }.await; - tx.commit().await?; + if let Err(e) = outcome { + for filename in &stored_filenames { + let _ = data.public_storage().delete(filename).await; + } + return Err(e); + } Ok(HttpResponse::Created().json(ApiResponse { error: "".into(), @@ -940,7 +951,7 @@ pub async fn delete_attachment( if remaining == 0 { // TODO: implement a cleanup job for orphaned attachment files // If this fails it's fine to still return a 204 to the user - let r = data.static_storage().delete(&filename).await; + let r = data.public_storage().delete(&filename).await; if let Err(e) = r { error!("Failed to delete attachment: {e}"); } From 9e90f6bb4ae467b5864c9dd4989f6b6bb6a5bb90 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Mon, 22 Jun 2026 22:22:02 +0300 Subject: [PATCH 33/69] feat(threads): return comment attachments ids --- src/endpoints/mod_version_submissions.rs | 22 +++++++++++++++++----- src/types/models/mod_version_submission.rs | 14 +++++++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 1935e42a..6c2e4107 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -21,7 +21,7 @@ use log::error; use serde::Deserialize; use sqlx::{Acquire, PgConnection}; use std::collections::HashMap; -use utoipa::IntoParams; +use utoipa::{IntoParams, ToSchema}; fn sanitize_comment(raw: &str) -> String { ammonia::Builder::default() @@ -299,11 +299,11 @@ pub async fn get_comments( .map(|(k, v)| { let transformed = v .into_iter() - .map(|i| data.public_storage().asset_url(&i.filename)) + .map(|i| (i.id, data.public_storage().asset_url(&i.filename))) .collect(); (k, transformed) }) - .collect::>>(); + .collect::>>(); let comments = rows .into_iter() @@ -550,8 +550,8 @@ pub async fn update_comment( mod_version_submissions::get_attachments_for_comment(path.comment_id, &mut tx) .await? .into_iter() - .map(|i| data.public_storage().asset_url(&i.filename)) - .collect::>(); + .map(|i| (i.id, data.public_storage().asset_url(&i.filename))) + .collect::>(); tx.commit().await?; @@ -703,12 +703,24 @@ pub async fn get_attachments( })) } +/// Documentation-only struct +#[derive(ToSchema)] +#[allow(dead_code)] +pub struct UploadAttachmentsForm { + #[schema(value_type = Vec, format = Binary)] + image: Vec> +} + /// Upload attachments to a submission comment #[utoipa::path( post, path = "/v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments", tag = "mod_version_submissions", params(CommentPath), + request_body( + content = UploadAttachmentsForm, + content_type = "multipart/form-data" + ), responses( (status = 201, description = "Attachments uploaded", body = inline(ApiResponse>)), (status = 400, description = "Bad request - no images, file too large, or attachment limit exceeded"), diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index 6f0a0dc6..03796255 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -50,13 +50,19 @@ pub struct ModVersionSubmissionComment { pub submission_id: i32, pub comment: String, pub author: Developer, - pub attachments: Vec, + pub attachments: Vec, #[serde(with = "chrono_dt_secs")] pub created_at: DateTime, #[serde(with = "chrono_dt_secs::option")] pub updated_at: Option>, } +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct ModVersionSubmissionCommentAttachment { + pub id: i64, + pub url: String +} + pub struct ModVersionSubmissionCommentRow { pub id: i64, pub submission_id: i32, @@ -70,14 +76,16 @@ impl ModVersionSubmissionCommentRow { pub fn into_comment( self, author: Developer, - attachment_links: Vec, + attachments: Vec<(i64, String)>, ) -> ModVersionSubmissionComment { ModVersionSubmissionComment { id: self.id, submission_id: self.submission_id, comment: self.comment, author, - attachments: attachment_links, + attachments: attachments.into_iter() + .map(|(id, url)| ModVersionSubmissionCommentAttachment {id, url}) + .collect(), created_at: self.created_at, updated_at: self.updated_at, } From db3b23bceb2845537f395475bcff063ebdd2ddf9 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Mon, 22 Jun 2026 23:15:36 +0300 Subject: [PATCH 34/69] feat(threads): turn submission get into find-or-create --- src/endpoints/mod_version_submissions.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 6c2e4107..12f03aed 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -8,6 +8,7 @@ use crate::storage::StorageDisk; use crate::types::api::{ApiResponse, PaginatedData}; use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::developer::Developer; +use crate::types::models::mod_version_status::ModVersionStatusEnum; use crate::types::models::mod_version_submission::{ CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, ModVersionSubmissionComment, ModVersionSubmissionLock, UpdateCommentPayload, @@ -90,7 +91,7 @@ async fn check_submission_lock( }) } -/// Get the submission for a mod version +/// Get the submission for a mod version, or create one if the mod version is pending and has no submission #[utoipa::path( get, path = "/v1/mods/{id}/versions/{version}/submission", @@ -110,25 +111,34 @@ pub async fn get_submission( ) -> Result { let mut pool = data.db().acquire().await?; - if !mods::exists(&path.id, &mut pool).await? { + let mut tx = pool.begin().await?; + + if !mods::exists(&path.id, &mut tx).await? { return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version_id = resolve_version_id(&path.id, &path.version, &mut pool).await?; + let version = mod_versions::get_by_version_str(&path.id, &path.version, &mut tx).await? + .ok_or_else(|| ApiError::NotFound("Version doesn't exist".into()))?; - let row = mod_version_submissions::get_for_mod_version(version_id, &mut pool) - .await? - .ok_or_else(|| ApiError::NotFound("Submission not found".into()))?; + let mut row = mod_version_submissions::get_for_mod_version(version.id, &mut tx).await?; + + if let None = row && version.status == ModVersionStatusEnum::Pending { + row = Some(mod_version_submissions::create(version.id, &mut tx).await?); + } + + let row = row.ok_or(ApiError::NotFound("Submission not found".into()))?; let locked_by = match row.locked_by { Some(dev_id) => Some( - developers::get_one(dev_id, &mut pool) + developers::get_one(dev_id, &mut tx) .await? .ok_or_else(|| ApiError::InternalError("Locked-by developer not found".into()))?, ), None => None, }; + tx.commit().await?; + Ok(web::Json(ApiResponse { error: "".into(), payload: row.into_submission(locked_by), From 580914958292a9bf2280bc1ca595a08fe50f8f53 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 9 Jul 2026 19:04:51 +0300 Subject: [PATCH 35/69] feat: add local file server for dev --- Cargo.lock | 36 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++++ src/main.rs | 11 +++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 730376ec..8e985100 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,29 @@ dependencies = [ "smallvec", ] +[[package]] +name = "actix-files" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8c4f30e3272d7c345f88ae0aac3848507ef5ba871f9cc2a41c8085a0f0523b" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags", + "bytes", + "derive_more 2.1.1", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + [[package]] name = "actix-http" version = "3.12.1" @@ -1519,6 +1542,7 @@ name = "geode-index" version = "0.53.2" dependencies = [ "actix-cors", + "actix-files", "actix-multipart", "actix-web", "ammonia", @@ -1751,6 +1775,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -4487,6 +4517,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "validator" version = "0.20.0" diff --git a/Cargo.toml b/Cargo.toml index 17202e0b..0d294b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,3 +48,7 @@ urlencoding = "2.1.3" validator = { version = "0.20.0", features = ["derive"] } bytes = "1.11" ammonia = "4" +actix-files = { version = "0.6", optional = true } + +[features] +dev-tools = ["dep:actix-files"] diff --git a/src/main.rs b/src/main.rs index a6bf1ad7..0b939094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,7 @@ async fn main() -> anyhow::Result<()> { let server = HttpServer::new(move || { let openapi = ApiDoc::openapi(); - App::new() + let app = App::new() .app_data(web::Data::new(app_data.clone())) .app_data(QueryConfig::default().error_handler(api::query_error_handler)) .service( @@ -62,7 +62,14 @@ async fn main() -> anyhow::Result<()> { .supports_credentials() .max_age(3600), ) - .wrap(Logger::default()) + .wrap(Logger::default()); + + #[cfg(feature = "dev-tools")] + let app = app + .service(actix_files::Files::new("/static", "storage/static")) + .service(actix_files::Files::new("/storage", "storage/public")); + + app .service(endpoints::mods::index) .service(endpoints::mods::get_mod_updates) .service(endpoints::mods::get) From 41823c34f0a9b1ecaba8d1c630e959828c19f452 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 9 Jul 2026 20:14:45 +0300 Subject: [PATCH 36/69] chore: version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e985100..68eaeb2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "geode-index" -version = "0.53.2" +version = "0.54.0" dependencies = [ "actix-cors", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 0d294b3b..ee73729c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geode-index" -version = "0.53.2" +version = "0.54.0" edition = "2024" [dependencies] From fc54fba6d06c1d7cb7effd57172e3b8a854fb56b Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 9 Jul 2026 21:07:48 +0300 Subject: [PATCH 37/69] fix: acces --- src/endpoints/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 6ed739d0..32ad388e 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -20,7 +20,7 @@ pub mod mod_version_submissions; pub enum ApiError { #[error("Authentication error: {0}")] Authentication(#[from] crate::auth::AuthenticationError), - #[error("You do not have acces to this resource")] + #[error("You do not have access to this resource")] Authorization, #[error("{0}")] Database(#[from] crate::database::DatabaseError), From 2e8cd044a2c25a8484ed83a52827941ac740841b Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 9 Jul 2026 21:08:16 +0300 Subject: [PATCH 38/69] chore: version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68eaeb2f..78430c69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "geode-index" -version = "0.54.0" +version = "0.54.1" dependencies = [ "actix-cors", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index ee73729c..a0c3f995 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geode-index" -version = "0.54.0" +version = "0.54.1" edition = "2024" [dependencies] From f817ce40a1408b04feb8f62c2bd5b370b576a544 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 9 Jul 2026 21:34:30 +0300 Subject: [PATCH 39/69] fix: only allow verified+ devs to comment --- src/endpoints/mod_version_submissions.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 12f03aed..a6c27d35 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -735,6 +735,7 @@ pub struct UploadAttachmentsForm { (status = 201, description = "Attachments uploaded", body = inline(ApiResponse>)), (status = 400, description = "Bad request - no images, file too large, or attachment limit exceeded"), (status = 401, description = "Unauthorized"), + (status = 403, description = "Unauthorized (only verified / mod developers / index admins can post attachments)"), (status = 404, description = "Mod, version, submission, or comment not found"), ), security(("bearer_token" = [])) @@ -778,6 +779,11 @@ pub async fn upload_attachments( return Err(ApiError::Authorization); } + let dev_of_mod = developers::has_access_to_mod(dev.id, &path.id, &mut tx).await?; + if !dev.verified && !dev.admin && !dev_of_mod { + return Err(ApiError::Authorization); + } + let existing = mod_version_submissions::count_attachments_for_comment(path.comment_id, &mut tx).await?; From 60aefd2ba55ec64a1d6e625cca6b872a1020dc02 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 01:18:47 +0300 Subject: [PATCH 40/69] docs: add actual ports to .env.example app url --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 1b5853bd..a70b276b 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ # Set to 1 to run using 1 thread APP_DEBUG=0 # Used for URL generation in downloads -APP_URL=http://localhost -FRONT_URL=http://localhost +APP_URL=http://localhost:3000 +FRONT_URL=http://localhost:5173 DATABASE_URL=postgres://user:password@localhost/schema PORT=8080 From ac3158b85b9f0dd32be0c7b35eadfc47dc250564 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 01:20:10 +0300 Subject: [PATCH 41/69] chore: improve docker image - added metadata - added a healthcheck - use rust image for build tools instead of debian directly - lock uid:gid to 1000:1000 --- .github/workflows/publish-docker-image.yaml | 12 +++++- Dockerfile | 47 +++++++++++++++------ src/endpoints/health.rs | 4 +- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/.github/workflows/publish-docker-image.yaml b/.github/workflows/publish-docker-image.yaml index 994f85c3..44f69719 100644 --- a/.github/workflows/publish-docker-image.yaml +++ b/.github/workflows/publish-docker-image.yaml @@ -83,6 +83,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Resolve build metadata + id: meta + run: | + echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" + echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Build and push by digest id: build uses: docker/build-push-action@v6 @@ -90,7 +96,11 @@ jobs: context: . push: true platforms: ${{ matrix.platform }} - build-args: LIBC=${{ matrix.libc }} + build-args: | + LIBC=${{ matrix.libc }} + VERSION=${{ needs.resolve-tag.outputs.tag }} + REVISION=${{ steps.meta.outputs.revision }} + BUILD_DATE=${{ steps.meta.outputs.date }} # Push to a digest-only ref (no tag) so the merge job can assemble # the final multi-arch manifest cleanly outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true diff --git a/Dockerfile b/Dockerfile index fe667f54..62769d24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,32 +3,32 @@ ARG LIBC=musl # 1. shared toolchain (Rust + Zig + cargo-chef + cargo-zigbuild) -FROM --platform=$BUILDPLATFORM debian:trixie-slim AS builder-tools +# rust:1.97.0-slim-trixie +FROM --platform=$BUILDPLATFORM rust@sha256:7284e7501ed1b80ae3d2f826024e8384749bb860c46d7989d3b70033b56bf31e AS builder-tools ARG ZIG_VERSION=0.15.2 -ARG CARGO_ZIGBUILD_VERSION=0.22.1 +ARG ZIG_SHA256=02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239 +ARG CARGO_ZIGBUILD_VERSION=0.23.0 ARG CARGO_CHEF_VERSION=0.1.77 -ENV CARGO_HOME=/cargo \ - RUSTUP_HOME=/rustup \ - PATH=/cargo/bin:/zig:$PATH +ENV PATH=/zig:$PATH RUN apt-get update && apt-get install -y --no-install-recommends \ pkg-config ca-certificates curl xz-utils build-essential \ && rm -rf /var/lib/apt/lists/* -# Install Rust (minimal profile, stable toolchain) -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain stable \ - && rustup target add \ +RUN rustup target add \ x86_64-unknown-linux-musl \ aarch64-unknown-linux-musl \ x86_64-unknown-linux-gnu \ aarch64-unknown-linux-gnu # Install Zig (used by cargo-zigbuild as the cross-linker) -RUN curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz" \ - | tar -xJ \ - && mv "zig-x86_64-linux-${ZIG_VERSION}" /zig +RUN curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz" -o /tmp/zig.tar.xz \ + && echo "${ZIG_SHA256} /tmp/zig.tar.xz" | sha256sum -c - \ + && tar -xJf /tmp/zig.tar.xz \ + && mv "zig-x86_64-linux-${ZIG_VERSION}" /zig \ + && rm /tmp/zig.tar.xz # Install cargo-chef and cargo-zigbuild RUN cargo install --locked cargo-chef --version ${CARGO_CHEF_VERSION} \ @@ -90,18 +90,21 @@ COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations COPY config ./config -RUN addgroup -S geode && adduser -S geode -G geode \ +RUN addgroup -S -g 1000 geode && adduser -S -u 1000 geode -G geode \ + && mkdir -p storage \ && chown -R geode:geode /app USER geode EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q --spider "http://127.0.0.1:${PORT:-3000}/" || exit 1 ENTRYPOINT ["./geode-index"] # 4b. Debian slim runtime (glibc / dynamically linked) FROM debian:trixie-slim AS runtime-gnu RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates tzdata \ + ca-certificates tzdata wget \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -109,12 +112,28 @@ COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations COPY config ./config -RUN groupadd --system geode && useradd --system --gid geode geode \ +RUN groupadd --system --gid 1000 geode && useradd --system --uid 1000 --gid geode geode \ + && mkdir -p storage \ && chown -R geode:geode /app USER geode EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q --spider "http://127.0.0.1:${PORT:-3000}/" || exit 1 ENTRYPOINT ["./geode-index"] FROM runtime-${LIBC:-musl} AS runtime ARG LIBC + +# Overriden in CI +ARG VERSION=dev +ARG REVISION=unknown +ARG BUILD_DATE=unknown + +LABEL org.opencontainers.image.title="geode-index" \ + org.opencontainers.image.description="Geode SDK mod index server" \ + org.opencontainers.image.source="https://github.com/geode-sdk/server" \ + org.opencontainers.image.documentation="https://github.com/geode-sdk/server#environment-variables" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.revision="${REVISION}" \ + org.opencontainers.image.created="${BUILD_DATE}" diff --git a/src/endpoints/health.rs b/src/endpoints/health.rs index 5f45011e..cbd22216 100644 --- a/src/endpoints/health.rs +++ b/src/endpoints/health.rs @@ -1,4 +1,4 @@ -use actix_web::get; +use actix_web::route; /// Health check endpoint #[utoipa::path( @@ -9,7 +9,7 @@ use actix_web::get; (status = 200, description = "Service is healthy", body = String, content_type = "text/plain") ) )] -#[get("/")] +#[route("/", method = "GET", method = "HEAD")] pub async fn health() -> &'static str { r#" _____ _____ From dc41c861f23e0069a5d675f1b4b0df4ad682c746 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 01:20:17 +0300 Subject: [PATCH 42/69] docs: improve docker documentation --- README.md | 94 +++++++++++++++++++++--- docs/examples/docker-compose.example.yml | 39 ++++++++++ docs/examples/nginx.example.conf | 38 ++++++++++ 3 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 docs/examples/docker-compose.example.yml create mode 100644 docs/examples/nginx.example.conf diff --git a/README.md b/README.md index 09be47a4..e4766530 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,97 @@ Server for the Geode Index, the API used by Geode SDK to retrieve mod information. Based on actix-web and sqlx. -## Requirements for hosting +## API Documentation -- rust stable -- PostgreSQL 17 or later +The API documentation can be found [here](https://api.geode-sdk.org/swagger/). A machine readable openapi.json specification can be found [here](https://api.geode-sdk.org/swagger/openapi.json). -## Documentation +## Running the server -The API documentation can be found [here](https://api.geode-sdk.org/swagger/). A machine readable openapi.json specification can be found [here](https://api.geode-sdk.org/swagger/openapi.json). +Requires PostgreSQL 17 or later. Two ways to run the server: + +- **Docker** (recommended for hosting): see [Running with Docker](#running-with-docker) below. No Rust toolchain needed. +- **From source** (recommended for development): requires the Rust stable toolchain. See the [development environment setup](docs/dev_setup.md) to get started, or just run: + ```bash + cargo build # or cargo build --release + ``` + +## Running with Docker -## Configuration +Images are published to GHCR with two variants per version, differing only in base OS/libc: -Check out the [development environment setup](docs/dev_setup.md) to get started! +- `-alpine`: musl, statically linked, smaller image (recommended default) +- `-trixie`: glibc, Debian-based -## Building the server +Available as `latest-alpine`/`latest-trixie`, or pinned to a version, e.g. `0.50.0-alpine`/`0.50.0-trixie`. ```bash -cargo build # or cargo build --release +docker run -p 3000:3000 \ + -e DATABASE_URL=postgres://user:password@host/schema \ + -e PORT=3000 \ + ghcr.io/geode-sdk/server:latest-alpine ``` + +Database migrations run automatically on startup, no separate migration step needed. + +The image has a built-in `HEALTHCHECK` that polls `GET /` (expects `200`); the same endpoint works for an external load balancer/orchestrator health check if you're not relying on Docker's own. + +### Environment variables + +| Variable | Required | Default | Description | +| --- | --- | --- | --- | +| `DATABASE_URL` | yes | - | PostgreSQL connection string: `postgres://{username}:{password}@{host}/{database}` | +| `DATABASE_CONNECTIONS` | no | `10` | Max size of the Postgres connection pool | +| `PORT` | no | `8080` | Port the server listens on | +| `APP_DEBUG` | no | `0` | Set to `1` to run single-threaded, for step debugging | +| `APP_URL` | no | `http://localhost:3000` | Used for URL generation in downloads | +| `FRONT_URL` | no | `http://localhost:5173` | Used for link generation (mod status badges, GitHub auth redirects) | +| `GITHUB_CLIENT_ID` | no | - | GitHub OAuth app client ID, used for login | +| `GITHUB_CLIENT_SECRET` | no | - | GitHub OAuth app client secret | +| `DISCORD_WEBHOOK_URL` | no | - | Webhook URL for general index notifications | +| `INDEX_ADMIN_DISCORD_WEBHOOK_URL` | no | - | Webhook URL for admin-only notifications | +| `MAX_MOD_FILESIZE_MB` | no | `250` | Max accepted mod upload size, in MB | +| `DISABLE_DOWNLOAD_COUNTS` | no | `0` | Set to `1` to globally disable download counting (e.g. during abuse) | + +### Persisting uploaded files + +The server writes uploaded files to `/app/storage` inside the container (static assets, public downloads, private files). This is **not** persisted by default, if you remove/recreate the container without a volume, uploads are lost. + +> [!IMPORTANT] +> The index does not serve these files itself in production. You are responsible for serving them with your own reverse proxy: `/app/storage/static` must be served at `/static/`, and `/app/storage/public` at `/storage/`, both relative to `APP_URL`. + +The container runs as a fixed non-root user, UID `1000`, GID `1000`. + +#### Production example: app + nginx via Compose + +A common production setup is to run the app alongside an nginx container sharing the storage volume, with nginx serving `/static` and `/storage` directly and proxying everything else through. This also sidesteps file permission issues you'd otherwise hit trying to read the app's (UID `1000`-owned) storage volume from a host-level reverse proxy. + +See [`docs/examples/docker-compose.example.yml`](docs/examples/docker-compose.example.yml) and [`docs/examples/nginx.example.conf`](docs/examples/nginx.example.conf). Your host-level reverse proxy (if any) just forwards to nginx's published port. + +#### Other options + +**Named volume**: the directory ownership is baked into the image, so this works with no extra setup: + +```bash +docker volume create geode-storage + +docker run -p 3000:3000 \ + -e DATABASE_URL=postgres://user:password@host/schema \ + -e PORT=3000 \ + -v geode-storage:/app/storage \ + ghcr.io/geode-sdk/server:latest-alpine +``` + +**Bind mount**: the host directory must be owned by UID/GID `1000` yourself, since Docker doesn't touch bind-mounted host paths' permissions: + +```bash +mkdir -p ./geode-storage +chown -R 1000:1000 ./geode-storage + +docker run -p 3000:3000 \ + -e DATABASE_URL=postgres://user:password@host/schema \ + -e PORT=3000 \ + -v ./geode-storage:/app/storage \ + ghcr.io/geode-sdk/server:latest-alpine +``` + +If your host already has a user/group with UID/GID `1000` (common for the first non-root user on Linux), you may already own the directory by default and can skip the `chown`. diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml new file mode 100644 index 00000000..b82878c0 --- /dev/null +++ b/docs/examples/docker-compose.example.yml @@ -0,0 +1,39 @@ +# Example production setup: app + nginx sharing a storage volume. +# +# See nginx.example.conf for the corresponding nginx config, and the +# "Running with Docker" section of the README for environment variables. + +services: + app: + image: ghcr.io/geode-sdk/server:latest-alpine + restart: unless-stopped + environment: + DATABASE_URL: postgres://user:password@host/schema + PORT: "3000" + APP_URL: https://api.sapphire-sdk.org + FRONT_URL: https://sapphire-sdk.org + volumes: + - geode-storage:/app/storage + networks: + - geode + + nginx: + image: nginx:1.27-alpine + restart: unless-stopped + depends_on: + - app + volumes: + - geode-storage:/app/storage:ro + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + # include only if you want to make this available on the host machine + # (if your reverse proxy is on the host, not inside docker) + # ports: + # - "127.0.0.1:8080:80" + networks: + - geode + +volumes: + geode-storage: + +networks: + geode: diff --git a/docs/examples/nginx.example.conf b/docs/examples/nginx.example.conf new file mode 100644 index 00000000..661056ff --- /dev/null +++ b/docs/examples/nginx.example.conf @@ -0,0 +1,38 @@ +# Example nginx config for docker-compose.example.yml. Serves the app's +# storage directly (avoiding the app process itself), proxies everything +# else through to the app service. +# +# This config doesn't handle HTTPS, that should be done by your actual +# reverse proxy. +# +# Paths must match src/main.rs's actix_files mounts: +# /static -> storage/static +# /storage -> storage/public + +server { + listen 80; + + location /static/ { + alias /app/storage/static/; + } + + location /storage/ { + alias /app/storage/public/; + autoindex off; + + # content-addressed files safe to cache forever + # since the hash is the filename + location ~ "^/storage/(.*/[a-f0-9]{2}/[a-f0-9]{64}(?:\.[a-z0-9]+)?)/?$" { + alias /app/storage/public/$1; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + } + + location / { + proxy_pass http://app:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} From 83540723cd2a92d7c49bf89cdf3e19f5f173f6ea Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 01:55:56 +0300 Subject: [PATCH 43/69] feat: basic migration to tracing --- Cargo.lock | 272 ++++++++---------- Cargo.toml | 12 +- src/auth/github.rs | 24 +- src/database/repository/auth_tokens.rs | 8 +- src/database/repository/dependencies.rs | 4 +- src/database/repository/deprecations.rs | 20 +- src/database/repository/developers.rs | 36 +-- .../repository/github_login_attempts.rs | 10 +- src/database/repository/github_web_logins.rs | 6 +- src/database/repository/incompatibilities.rs | 4 +- src/database/repository/mod_downloads.rs | 6 +- src/database/repository/mod_gd_versions.rs | 4 +- src/database/repository/mod_links.rs | 2 +- src/database/repository/mod_tags.rs | 12 +- .../repository/mod_version_statuses.rs | 2 +- .../repository/mod_version_submissions.rs | 42 +-- src/database/repository/mod_versions.rs | 20 +- src/database/repository/mods.rs | 26 +- src/database/repository/refresh_tokens.rs | 8 +- src/endpoints/auth/github.rs | 2 +- src/endpoints/mod_version_submissions.rs | 6 +- src/endpoints/mod_versions.rs | 4 +- src/jobs/migrate.rs | 2 +- src/logging.rs | 34 +++ src/main.rs | 10 +- src/mod_zip.rs | 16 +- src/types/mod_json.rs | 12 +- src/types/models/dependency.rs | 2 +- src/types/models/incompatibility.rs | 4 +- src/types/models/mod_entity.rs | 16 +- src/types/models/mod_gd_version.rs | 4 +- src/types/models/mod_link.rs | 4 +- src/types/models/mod_version.rs | 12 +- src/types/models/stats.rs | 4 +- src/types/models/tag.rs | 6 +- src/webhook/discord.rs | 6 +- 36 files changed, 331 insertions(+), 331 deletions(-) create mode 100644 src/logging.rs diff --git a/Cargo.lock b/Cargo.lock index 78430c69..97149804 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,15 +413,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "arc-swap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" -dependencies = [ - "rustversion", -] - [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -1160,12 +1151,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "destructure_traitobject" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" - [[package]] name = "digest" version = "0.10.7" @@ -1553,8 +1538,6 @@ dependencies = [ "dotenvy", "futures", "image", - "log", - "log4rs", "lzma-rust2", "moka", "regex", @@ -1566,6 +1549,9 @@ dependencies = [ "sqlx", "thiserror 2.0.18", "tokio", + "tracing", + "tracing-actix-web", + "tracing-subscriber", "urlencoding", "utoipa", "utoipa-swagger-ui", @@ -1793,12 +1779,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - [[package]] name = "hybrid-array" version = "0.4.11" @@ -2315,44 +2295,6 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "serde_core", -] - -[[package]] -name = "log-mdc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" - -[[package]] -name = "log4rs" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c" -dependencies = [ - "anyhow", - "arc-swap", - "chrono", - "derive_more 2.1.1", - "fnv", - "humantime", - "libc", - "log", - "log-mdc", - "mock_instant", - "parking_lot", - "rand 0.9.4", - "serde", - "serde-value", - "serde_json", - "serde_yaml", - "thiserror 2.0.18", - "thread-id", - "typemap-ors", - "unicode-segmentation", - "winapi", -] [[package]] name = "loop9" @@ -2413,6 +2355,15 @@ dependencies = [ "syn", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2477,12 +2428,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "mock_instant" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" - [[package]] name = "moka" version = "0.12.15" @@ -2513,6 +2458,12 @@ dependencies = [ "pxfm", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2543,6 +2494,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2645,15 +2605,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "parking" version = "2.2.1" @@ -2778,6 +2729,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3519,16 +3490,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -3583,19 +3544,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha1" version = "0.10.6" @@ -3642,6 +3590,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -4097,13 +4054,12 @@ dependencies = [ ] [[package]] -name = "thread-id" -version = "5.1.0" +name = "thread_local" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2010d27add3f3240c1fef7959f46c814487b216baee662af53be645ba7831c07" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ - "libc", - "windows-sys 0.61.2", + "cfg-if", ] [[package]] @@ -4301,6 +4257,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-actix-web" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bb7a33ce7f0807d44124b5119ea3581fd59028db56477a7aa01741c869ca6a" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -4319,6 +4288,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -4333,15 +4345,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" -[[package]] -name = "typemap-ors" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" -dependencies = [ - "unsafe-any-ors", -] - [[package]] name = "typenum" version = "1.20.0" @@ -4393,21 +4396,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-any-ors" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" -dependencies = [ - "destructure_traitobject", -] - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -4553,6 +4541,12 @@ dependencies = [ "syn", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4760,22 +4754,6 @@ dependencies = [ "wasite", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -4785,12 +4763,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index a0c3f995..c6bb3c98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ actix-multipart = "0.7" actix-web = "4.10" anyhow = "1.0" dotenvy = "0.15" -log = "0.4" futures = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -32,14 +31,9 @@ clap = { version = "4.5", features = ["derive"] } regex = "1.10" chrono = { version = "0.4", features = ["default", "serde"] } actix-cors = "0.7" -log4rs = { version = "1.3.0", features = [ - "console_appender", - "rolling_file_appender", - "fixed_window_roller", - "time_trigger", - "default", - "threshold_filter", -] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } +tracing-actix-web = "0.7" thiserror = "2.0.12" moka = { version = "0.12.13", features = ["future"] } utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "uuid"] } diff --git a/src/auth/github.rs b/src/auth/github.rs index e29f416f..52a1838c 100644 --- a/src/auth/github.rs +++ b/src/auth/github.rs @@ -108,10 +108,10 @@ impl GithubClient { })) .send() .await - .inspect_err(|e| log::error!("Failed to start OAuth device flow with GitHub: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to start OAuth device flow with GitHub: {e}"))?; if !res.status().is_success() { - log::error!( + tracing::error!( "GitHub OAuth device flow failed to start. Error code: {}. Body: {}", res.status(), res.text().await.unwrap_or("No body received".into()) @@ -125,7 +125,7 @@ impl GithubClient { .json::() .await .inspect_err(|e| { - log::error!("Failed to parse OAuth device flow response from GitHub: {e}") + tracing::error!("Failed to parse OAuth device flow response from GitHub: {e}") }) .or(Err(AuthenticationError::InternalError( "Failed to parse response from GitHub".into(), @@ -183,18 +183,18 @@ impl GithubClient { .send() .await .inspect_err(|e| { - log::error!("Failed to poll GitHub for developer access token: {e}") + tracing::error!("Failed to poll GitHub for developer access token: {e}") })?; Ok(resp .json::() .await - .inspect_err(|e| log::error!("Failed to decode GitHub response: {e}"))? + .inspect_err(|e| tracing::error!("Failed to decode GitHub response: {e}"))? .get("access_token") .ok_or(AuthenticationError::UserAuthPending)? .as_str() .ok_or_else(|| { - log::error!("Invalid access_token received from GitHub"); + tracing::error!("Invalid access_token received from GitHub"); AuthenticationError::InternalError( "Failed to retrieve access token from GitHub".into(), ) @@ -211,7 +211,7 @@ impl GithubClient { .await?; if !resp.status().is_success() { - log::error!( + tracing::error!( "github::get_user: received non-2xx response: {}. Body: {}", resp.status(), resp.text().await.unwrap_or("No response body".into()) @@ -223,7 +223,7 @@ impl GithubClient { resp.json::() .await - .inspect_err(|e| log::error!("github::get_user: failed to parse response: {e}")) + .inspect_err(|e| tracing::error!("github::get_user: failed to parse response: {e}")) .or(Err(AuthenticationError::InternalError( "Failed to parse user JSON received from GitHub".into(), ))) @@ -242,11 +242,11 @@ impl GithubClient { .send() .await .inspect_err(|e| { - log::error!("github::get_installation: failed to fetch repositories: {e}") + tracing::error!("github::get_installation: failed to fetch repositories: {e}") })?; if !resp.status().is_success() { - log::error!( + tracing::error!( "github::get_installation: received non-2xx response: {}. Body: {}", resp.status(), resp.text().await.unwrap_or("No response body".into()) @@ -259,7 +259,7 @@ impl GithubClient { let body = resp .json::() .await - .inspect_err(|e| log::error!("github::get_installation: failed to parse response: {e}")) + .inspect_err(|e| tracing::error!("github::get_installation: failed to parse response: {e}")) .or(Err(AuthenticationError::InternalError( "Failed to parse response from GitHub".into(), )))?; @@ -285,7 +285,7 @@ impl GithubClient { serde_json::from_value(owner) .inspect_err(|e| { - log::error!( + tracing::error!( "github::get_installation: failed to extract owner from serde_json value: {e}" ) }) diff --git a/src/database/repository/auth_tokens.rs b/src/database/repository/auth_tokens.rs index 27091103..e3953aaf 100644 --- a/src/database/repository/auth_tokens.rs +++ b/src/database/repository/auth_tokens.rs @@ -29,7 +29,7 @@ pub async fn generate_token( .execute(&mut *conn) .await .inspect_err(|e| { - log::error!("Failed to insert auth_token for developer {developer_id}: {e}") + tracing::error!("Failed to insert auth_token for developer {developer_id}: {e}") })?; Ok(token) @@ -45,7 +45,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to remove auth token: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to remove auth token: {e}"))?; Ok(()) } @@ -61,7 +61,7 @@ pub async fn remove_developer_tokens( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to wipe developer tokens: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to wipe developer tokens: {e}"))?; Ok(()) } @@ -73,7 +73,7 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { ) .execute(conn) .await - .inspect_err(|e| log::error!("Auth token cleanup failed: {e}"))?; + .inspect_err(|e| tracing::error!("Auth token cleanup failed: {e}"))?; Ok(()) } diff --git a/src/database/repository/dependencies.rs b/src/database/repository/dependencies.rs index e7846188..8b91544b 100644 --- a/src/database/repository/dependencies.rs +++ b/src/database/repository/dependencies.rs @@ -59,7 +59,7 @@ pub async fn create( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("dependenceis::create query failed: {e}")) + .inspect_err(|e| tracing::error!("dependenceis::create query failed: {e}")) .map_err(|e| e.into()) } @@ -71,7 +71,7 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError ) .execute(conn) .await - .inspect_err(|e| log::error!("dependencies::clear query failed: {e}")) + .inspect_err(|e| tracing::error!("dependencies::clear query failed: {e}")) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/deprecations.rs b/src/database/repository/deprecations.rs index 75861afa..2d87e72c 100644 --- a/src/database/repository/deprecations.rs +++ b/src/database/repository/deprecations.rs @@ -15,7 +15,7 @@ pub async fn get_for_mods( ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::get_for_mods failed: {e}"))?; + .inspect_err(|e| tracing::error!("deprecations::get_for_mods failed: {e}"))?; let mut bys: Vec<_> = sqlx::query!( "SELECT dby.deprecation_id, dby.by_mod_id @@ -25,7 +25,7 @@ pub async fn get_for_mods( ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::get_for_mods failed: {e}"))?; + .inspect_err(|e| tracing::error!("deprecations::get_for_mods failed: {e}"))?; Ok(deps .into_iter() @@ -53,7 +53,7 @@ pub async fn get(id: i32, conn: &mut PgConnection) -> Result ) .fetch_optional(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::get failed: {e}"))? + .inspect_err(|e| tracing::error!("deprecations::get failed: {e}"))? .map(|x| Deprecation { id, mod_id: x.mod_id, @@ -75,7 +75,7 @@ pub async fn get(id: i32, conn: &mut PgConnection) -> Result ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::get failed: {e}"))?; + .inspect_err(|e| tracing::error!("deprecations::get failed: {e}"))?; dep.by = deprecated_by.into_iter().map(|b| b.by_mod_id).collect(); @@ -99,7 +99,7 @@ pub async fn create( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::create failed: {e}"))? + .inspect_err(|e| tracing::error!("deprecations::create failed: {e}"))? .id; if !by.is_empty() { @@ -140,7 +140,7 @@ pub async fn update( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::update failed: {e}"))?; + .inspect_err(|e| tracing::error!("deprecations::update failed: {e}"))?; deprecation.reason = reason.to_string(); @@ -155,7 +155,7 @@ pub async fn update( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::update failed: {e}"))?; + .inspect_err(|e| tracing::error!("deprecations::update failed: {e}"))?; insert_deprecated_by(deprecation.id, by, &mut *conn).await?; @@ -186,7 +186,7 @@ pub async fn delete(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseErro ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::delete failed: {e}"))?; + .inspect_err(|e| tracing::error!("deprecations::delete failed: {e}"))?; Ok(()) } @@ -199,7 +199,7 @@ pub async fn clear_all(mod_id: &str, conn: &mut PgConnection) -> Result<(), Data ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::clear_all failed: {e}"))?; + .inspect_err(|e| tracing::error!("deprecations::clear_all failed: {e}"))?; Ok(()) } @@ -227,7 +227,7 @@ async fn insert_deprecated_by( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("deprecations::insert_deprecated_by failed: {e}")) + .inspect_err(|e| tracing::error!("deprecations::insert_deprecated_by failed: {e}")) .map(|_| ()) .map_err(|e| e.into()) } diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 16e759be..f60271be 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -37,7 +37,7 @@ pub async fn index( ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to fetch developers: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to fetch developers: {}", e))?; let count = index_count(query, &mut *conn).await?; @@ -62,7 +62,7 @@ pub async fn index_count( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to fetch developer count: {}", e)) + .inspect_err(|e| tracing::error!("Failed to fetch developer count: {}", e)) .map(|x| x.count.unwrap_or(0)) .map_err(|e| e.into()) } @@ -87,7 +87,7 @@ pub async fn fetch_or_insert_github( ) .fetch_optional(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to fetch developer for GitHub id: {e}"))? + .inspect_err(|e| tracing::error!("Failed to fetch developer for GitHub id: {e}"))? { Some(dev) => Ok(dev), None => Ok(insert_github(github_id, username, conn).await?), @@ -115,7 +115,7 @@ async fn insert_github( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to insert developer: {e}")) + .inspect_err(|e| tracing::error!("Failed to insert developer: {e}")) .map_err(|e| e.into()) } @@ -135,7 +135,7 @@ pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result> = HashMap::new(); @@ -282,7 +282,7 @@ pub async fn has_access_to_mod( .fetch_optional(&mut *conn) .await .inspect_err(|e| { - log::error!( + tracing::error!( "Failed to find mod {} access for developer {}: {}", mod_id, dev_id, @@ -306,7 +306,7 @@ pub async fn has_active_mod(dev_id: i32, conn: &mut PgConnection) -> Result Result Result<(), Databas ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to poll GitHub login attempt: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to poll GitHub login attempt: {e}"))?; Ok(()) } @@ -114,7 +114,7 @@ pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseE sqlx::query!("DELETE FROM github_login_attempts WHERE uid = $1", uuid) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to remove GitHub login attempt: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to remove GitHub login attempt: {e}"))?; Ok(()) } diff --git a/src/database/repository/github_web_logins.rs b/src/database/repository/github_web_logins.rs index cd8acf98..9c0e0e99 100644 --- a/src/database/repository/github_web_logins.rs +++ b/src/database/repository/github_web_logins.rs @@ -8,7 +8,7 @@ pub async fn create_unique(conn: &mut PgConnection) -> Result Result Result<(), DatabaseE sqlx::query!("DELETE FROM github_web_logins WHERE state = $1", uuid) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to delete GitHub web login secret: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to delete GitHub web login secret: {e}"))?; Ok(()) } diff --git a/src/database/repository/incompatibilities.rs b/src/database/repository/incompatibilities.rs index 8c43188a..8e415d07 100644 --- a/src/database/repository/incompatibilities.rs +++ b/src/database/repository/incompatibilities.rs @@ -64,7 +64,7 @@ pub async fn create( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("incompatibilities::create query failed: {e}")) + .inspect_err(|e| tracing::error!("incompatibilities::create query failed: {e}")) .map_err(|e| e.into()) } @@ -76,7 +76,7 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError ) .execute(conn) .await - .inspect_err(|e| log::error!("incompatibilities::clear query failed: {e}")) + .inspect_err(|e| tracing::error!("incompatibilities::clear query failed: {e}")) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/mod_downloads.rs b/src/database/repository/mod_downloads.rs index 4f64e157..8a93477f 100644 --- a/src/database/repository/mod_downloads.rs +++ b/src/database/repository/mod_downloads.rs @@ -18,7 +18,7 @@ pub async fn create( .execute(&mut *conn) .await .inspect_err(|e| { - log::error!("Failed to insert new download for mod_version id {mod_version_id}: {e}"); + tracing::error!("Failed to insert new download for mod_version id {mod_version_id}: {e}"); })?; Ok(result.rows_affected() > 0) @@ -40,7 +40,7 @@ pub async fn has_downloaded_mod( ) .fetch_optional(&mut *conn) .await - .inspect_err(|e| log::error!("mod_downloads::has_downloaded_mod query error: {e}")) + .inspect_err(|e| tracing::error!("mod_downloads::has_downloaded_mod query error: {e}")) .map_err(|e| e.into()) .map(|x| x.is_some()) } @@ -54,7 +54,7 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("mod_downloads::cleanup query failed: {e}"))?; + .inspect_err(|e| tracing::error!("mod_downloads::cleanup query failed: {e}"))?; Ok(()) } diff --git a/src/database/repository/mod_gd_versions.rs b/src/database/repository/mod_gd_versions.rs index 19efa36a..8d49bcb0 100644 --- a/src/database/repository/mod_gd_versions.rs +++ b/src/database/repository/mod_gd_versions.rs @@ -33,7 +33,7 @@ pub async fn create( ) .execute(conn) .await - .inspect_err(|e| log::error!("mod_gd_versions::create query failed: {e}"))?; + .inspect_err(|e| tracing::error!("mod_gd_versions::create query failed: {e}"))?; Ok(json.gd.clone()) } @@ -46,7 +46,7 @@ pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), D ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("incompatibilities::clear query failed: {e}")) + .inspect_err(|e| tracing::error!("incompatibilities::clear query failed: {e}")) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/mod_links.rs b/src/database/repository/mod_links.rs index 86547a59..8776cf69 100644 --- a/src/database/repository/mod_links.rs +++ b/src/database/repository/mod_links.rs @@ -26,7 +26,7 @@ pub async fn upsert( ) .execute(&mut *conn) .await - .inspect_err(|x| log::error!("Failed to upsert mod_links for id {mod_id}: {x}"))?; + .inspect_err(|x| tracing::error!("Failed to upsert mod_links for id {mod_id}: {x}"))?; Ok(ModLinks { mod_id: mod_id.into(), diff --git a/src/database/repository/mod_tags.rs b/src/database/repository/mod_tags.rs index e2987cc2..068332c7 100644 --- a/src/database/repository/mod_tags.rs +++ b/src/database/repository/mod_tags.rs @@ -15,7 +15,7 @@ pub async fn get_all_writable(conn: &mut PgConnection) -> Result, Datab ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("mod_tags::get_all_writeable failed: {e}"))? + .inspect_err(|e| tracing::error!("mod_tags::get_all_writeable failed: {e}"))? .into_iter() .map(|i| Tag { id: i.id, @@ -47,7 +47,7 @@ pub async fn get_allowed_for_mod( ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("mod_tags::get_allowed_for_mod failed: {e}"))? + .inspect_err(|e| tracing::error!("mod_tags::get_allowed_for_mod failed: {e}"))? .into_iter() .map(|i| Tag { id: i.id, @@ -73,7 +73,7 @@ pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("mod_tags::get_all failed: {e}"))? + .inspect_err(|e| tracing::error!("mod_tags::get_all failed: {e}"))? .into_iter() .map(|i| Tag { id: i.id, @@ -100,7 +100,7 @@ pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("mod_tags::get_tags failed: {e}")) + .inspect_err(|e| tracing::error!("mod_tags::get_tags failed: {e}")) .map_err(|e| e.into()) .map(|vec| { vec.into_iter() @@ -145,7 +145,7 @@ pub async fn update_for_mod( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to remove tags: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to remove tags: {e}"))?; } if !insertable.is_empty() { @@ -163,7 +163,7 @@ pub async fn update_for_mod( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to insert tags: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to insert tags: {e}"))?; } Ok(()) diff --git a/src/database/repository/mod_version_statuses.rs b/src/database/repository/mod_version_statuses.rs index 37c9defe..a9c0e4b5 100644 --- a/src/database/repository/mod_version_statuses.rs +++ b/src/database/repository/mod_version_statuses.rs @@ -19,7 +19,7 @@ pub async fn create( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("Failed to create mod_version_status: {e}")) + .inspect_err(|e| tracing::error!("Failed to create mod_version_status: {e}")) .map(|i| i.id) .map_err(|e| e.into()) } diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index b180625b..54b4931e 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -22,7 +22,7 @@ pub async fn get_for_mod_version( ) .fetch_optional(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::get_for_mod_versions failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::get_for_mod_versions failed: {e}")) .map_err(|e| e.into()) } @@ -40,7 +40,7 @@ pub async fn get_audit_for_submission( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::get_audit_for_submission failed: {e}",)) + .inspect_err(|e| tracing::error!("mod_version_submissions::get_audit_for_submission failed: {e}",)) .map_err(|e| e.into()) } @@ -57,7 +57,7 @@ pub async fn create( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::create failed: {e}"))?; + .inspect_err(|e| tracing::error!("mod_version_submissions::create failed: {e}"))?; insert_submission_audit(mod_version_id, AuditAction::Created, None, None, conn).await?; Ok(row) @@ -102,7 +102,7 @@ pub async fn set_locked( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::set_locked failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::set_locked failed: {e}")) .map_err(|e| e.into()) } @@ -128,7 +128,7 @@ pub async fn get_paginated_comments_for_submission( .fetch_all(conn) .await .inspect_err(|e| { - log::error!("mod_version_submissions::get_paginated_items_for_submission failed: {e}") + tracing::error!("mod_version_submissions::get_paginated_items_for_submission failed: {e}") }) .map_err(|e| e.into()) } @@ -144,7 +144,7 @@ pub async fn count_comments_for_submission( .fetch_one(conn) .await .inspect_err(|e| { - log::error!("mod_version_submissions::count_comments_for_submission failed: {e}") + tracing::error!("mod_version_submissions::count_comments_for_submission failed: {e}") }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) @@ -167,7 +167,7 @@ pub async fn create_comment( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::create_comment failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::create_comment failed: {e}")) .map_err(|e| e.into()) } @@ -184,7 +184,7 @@ pub async fn get_comment( ) .fetch_optional(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::get_comment failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::get_comment failed: {e}")) .map_err(|e| e.into()) } @@ -204,7 +204,7 @@ pub async fn update_comment( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::update_comment failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::update_comment failed: {e}")) .map_err(|e| e.into()) } @@ -218,7 +218,7 @@ pub async fn delete_comment( ) .execute(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::delete_comment failed: {e}"))?; + .inspect_err(|e| tracing::error!("mod_version_submissions::delete_comment failed: {e}"))?; Ok(result.rows_affected() > 0) } @@ -236,7 +236,7 @@ pub async fn get_audit_for_comment( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::get_audit_for_comment failed: {e}",)) + .inspect_err(|e| tracing::error!("mod_version_submissions::get_audit_for_comment failed: {e}",)) .map_err(|e| e.into()) } @@ -251,7 +251,7 @@ pub async fn count_attachments_for_comment( .fetch_one(conn) .await .inspect_err(|e| { - log::error!("mod_version_submissions::count_attachments_for_comment failed: {e}") + tracing::error!("mod_version_submissions::count_attachments_for_comment failed: {e}") }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) @@ -272,7 +272,7 @@ pub async fn create_attachment( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::create_attachment failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::create_attachment failed: {e}")) .map_err(|e| e.into()) } @@ -291,7 +291,7 @@ pub async fn get_attachments_for_comment( .fetch_all(conn) .await .inspect_err(|e| { - log::error!("mod_version_submissions::get_attachments_for_comment failed: {e}") + tracing::error!("mod_version_submissions::get_attachments_for_comment failed: {e}") }) .map_err(|e: Error| e.into()) } @@ -311,7 +311,7 @@ pub async fn get_attachments_for_comments( .fetch_all(conn) .await .inspect_err(|e| { - log::error!("mod_version_submissions::get_attachments_for_comments failed: {e}") + tracing::error!("mod_version_submissions::get_attachments_for_comments failed: {e}") })?; let mut ret: HashMap> = HashMap::with_capacity(comment_ids.len()); @@ -336,7 +336,7 @@ pub async fn get_attachment( ) .fetch_optional(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::get_attachment failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::get_attachment failed: {e}")) .map_err(|e| e.into()) } @@ -350,7 +350,7 @@ pub async fn delete_attachment( ) .execute(conn) .await - .inspect_err(|e| log::error!("mod_version_submissions::delete_attachment failed: {e}"))?; + .inspect_err(|e| tracing::error!("mod_version_submissions::delete_attachment failed: {e}"))?; Ok(result.rows_affected() > 0) } @@ -365,7 +365,7 @@ pub async fn count_references_to_filename( .fetch_one(conn) .await .inspect_err(|e| { - log::error!("mod_version_submissions::count_references_to_filename failed: {e}") + tracing::error!("mod_version_submissions::count_references_to_filename failed: {e}") }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) @@ -391,7 +391,7 @@ pub async fn count_references_to_filenames( .collect() }) .inspect_err(|e| { - log::error!("mod_version_submissions::count_references_to_filenames failed: {e}") + tracing::error!("mod_version_submissions::count_references_to_filenames failed: {e}") }) .map_err(|e| e.into()) } @@ -414,7 +414,7 @@ pub async fn insert_submission_audit( .execute(conn) .await .map(|_| ()) - .inspect_err(|e| log::error!("mod_version_submissions::insert_submission_audit failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::insert_submission_audit failed: {e}")) .map_err(|e| e.into()) } @@ -436,6 +436,6 @@ pub async fn insert_comment_audit( .execute(conn) .await .map(|_| ()) - .inspect_err(|e| log::error!("mod_version_submissions::insert_comment_audit failed: {e}")) + .inspect_err(|e| tracing::error!("mod_version_submissions::insert_comment_audit failed: {e}")) .map_err(|e| e.into()) } diff --git a/src/database/repository/mod_versions.rs b/src/database/repository/mod_versions.rs index fc6d4752..da9b9a89 100644 --- a/src/database/repository/mod_versions.rs +++ b/src/database/repository/mod_versions.rs @@ -83,7 +83,7 @@ pub async fn get_by_version_str( ) .fetch_optional(conn) .await - .inspect_err(|e| log::error!("Failed to get mod_version by version string: {e}")) + .inspect_err(|e| tracing::error!("Failed to get mod_version by version string: {e}")) .map_err(|e| e.into()) .map(|opt| opt.map(|x| x.into_mod_version())) } @@ -112,7 +112,7 @@ pub async fn get_for_mod( ) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to get mod_versions for mod {mod_id}: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to get mod_versions for mod {mod_id}: {e}"))?; let version_ids: Vec = records.iter().map(|x| x.id).collect(); let mut gd_versions = ModGDVersion::get_for_mod_versions(&version_ids, conn).await?; @@ -138,7 +138,7 @@ pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to increment downloads for mod_version {id}: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to increment downloads for mod_version {id}: {e}"))?; Ok(()) } @@ -151,7 +151,7 @@ pub async fn create_from_json( sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey DEFERRED") .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to update constraint: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to update constraint: {e}"))?; let geode = Version::parse(&json.geode).or(Err(DatabaseError::InvalidInput( "mod.json geode version is invalid semver".into(), @@ -192,7 +192,7 @@ pub async fn create_from_json( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to insert mod_version: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to insert mod_version: {e}"))?; let id = row.id; @@ -209,12 +209,12 @@ pub async fn create_from_json( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to set status: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to set status: {e}"))?; sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey IMMEDIATE") .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to update constraint: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to update constraint: {e}"))?; Ok(ModVersion { id, @@ -306,7 +306,7 @@ pub async fn update_pending_version( .fetch_one(&mut *conn) .await .inspect_err(|err| { - log::error!( + tracing::error!( "Failed to update pending version {}-{}: {err}", json.id, json.version @@ -322,7 +322,7 @@ pub async fn update_pending_version( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to update tag for mod: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to update tag for mod: {e}"))?; } Ok(ModVersion { @@ -379,7 +379,7 @@ pub async fn update_version_status( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to update mod_version_status: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to update mod_version_status: {e}"))?; version.status = status; diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index 54bc4d54..1dd03267 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -59,7 +59,7 @@ pub async fn get_one( ) .fetch_optional(conn) .await - .inspect_err(|e| log::error!("Failed to fetch mod {id}: {e}")) + .inspect_err(|e| tracing::error!("Failed to fetch mod {id}: {e}")) .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } else { @@ -74,7 +74,7 @@ pub async fn get_one( ) .fetch_optional(conn) .await - .inspect_err(|e| log::error!("Failed to fetch mod {}: {}", id, e)) + .inspect_err(|e| tracing::error!("Failed to fetch mod {}: {}", id, e)) .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } @@ -104,7 +104,7 @@ pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result Result Result = sqlx::query!("SELECT id FROM mods WHERE id = ANY($1)", ids) .fetch_all(&mut *conn) .await - .inspect_err(|e| log::error!("mods::exists_multiple failed: {e}"))? + .inspect_err(|e| tracing::error!("mods::exists_multiple failed: {e}"))? .into_iter() .map(|x| x.id) .collect(); @@ -228,7 +228,7 @@ pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result Result<() ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to increment downloads for mod {id}: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to increment downloads for mod {id}: {e}"))?; Ok(()) } @@ -274,7 +274,7 @@ pub async fn update_with_json( ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to update mod: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to update mod: {}", e))?; the_mod.repository = json.repository.clone(); the_mod.about = json.about.clone(); @@ -304,7 +304,7 @@ pub async fn update_with_json_moved( ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to update mod: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to update mod: {e}"))?; the_mod.repository = json.repository; the_mod.about = json.about; @@ -324,7 +324,7 @@ pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), D ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to touch created_at for mod {id}: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to touch created_at for mod {id}: {e}"))?; Ok(()) } diff --git a/src/database/repository/refresh_tokens.rs b/src/database/repository/refresh_tokens.rs index 2d643182..d1575042 100644 --- a/src/database/repository/refresh_tokens.rs +++ b/src/database/repository/refresh_tokens.rs @@ -20,7 +20,7 @@ pub async fn generate_token( ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to insert refresh token: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to insert refresh token: {e}"))?; Ok(token) } @@ -34,7 +34,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to remove refresh token: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to remove refresh token: {e}"))?; Ok(()) } @@ -50,7 +50,7 @@ pub async fn remove_developer_tokens( ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to remove refresh tokens: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to remove refresh tokens: {e}"))?; Ok(()) } @@ -62,7 +62,7 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { ) .execute(conn) .await - .inspect_err(|e| log::error!("Refresh token cleanup failed: {e}"))?; + .inspect_err(|e| tracing::error!("Refresh token cleanup failed: {e}"))?; Ok(()) } diff --git a/src/endpoints/auth/github.rs b/src/endpoints/auth/github.rs index f14aa222..687c5e67 100644 --- a/src/endpoints/auth/github.rs +++ b/src/endpoints/auth/github.rs @@ -219,7 +219,7 @@ pub async fn poll_github_login( let user = client .get_user(&token) .await - .inspect_err(|e| log::error!("Failed to fetch user from GitHub: {e}")) + .inspect_err(|e| tracing::error!("Failed to fetch user from GitHub: {e}")) .map_err(|_| ApiError::InternalError("Failed to fetch user data from GitHub".into()))?; let mut tx = pool.begin().await?; diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index a6c27d35..a935567b 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -18,7 +18,7 @@ use crate::webhook::discord::DiscordWebhook; use actix_multipart::Multipart; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use futures::StreamExt; -use log::error; +use tracing::error; use serde::Deserialize; use sqlx::{Acquire, PgConnection}; use std::collections::HashMap; @@ -650,7 +650,7 @@ pub async fn delete_comment( for filename in attachment_filenames { if !references.contains_key(&filename) && let Err(e) = data.public_storage().delete(&filename).await { - log::error!("Failed to delete attachment file {filename}: {e}"); + tracing::error!("Failed to delete attachment file {filename}: {e}"); } } @@ -843,7 +843,7 @@ pub async fn upload_attachments( .collect() }) .await - .inspect_err(|e| log::error!("Bad bad bad bad {e}")) + .inspect_err(|e| tracing::error!("Bad bad bad bad {e}")) .map_err(|_| ApiError::InternalError("Something very bad happened!".into()))??; let mut result = Vec::with_capacity(processed.len()); diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 4c956357..52383879 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -343,7 +343,7 @@ pub async fn create_version( let bytes = download_mod(&download_link, data.max_download_mb()).await?; let json = ModJson::from_zip(bytes, &download_link, make_accepted) - .inspect_err(|e| log::error!("Failed to parse mod.json: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to parse mod.json: {e}"))?; if json.id != the_mod.id { return Err(ApiError::BadRequest(format!( "Request id {} does not match mod.json id {}", @@ -367,7 +367,7 @@ pub async fn create_version( } else { let latest = versions.first().unwrap(); let latest_version = semver::Version::parse(&latest.version) - .inspect_err(|e| log::error!("Failed to parse locally stored version: {}", e)) + .inspect_err(|e| tracing::error!("Failed to parse locally stored version: {}", e)) .or(Err(ApiError::InternalError(format!( "Failed to parse semver for existing mod version: {}", &latest.version diff --git a/src/jobs/migrate.rs b/src/jobs/migrate.rs index 6abfb07e..d8c44147 100644 --- a/src/jobs/migrate.rs +++ b/src/jobs/migrate.rs @@ -2,7 +2,7 @@ use sqlx::PgConnection; pub async fn migrate(pool: &mut PgConnection) -> anyhow::Result<()> { if let Err(e) = sqlx::migrate!("./migrations").run(&mut *pool).await { - log::error!("Error encountered while running migrations: {}", e); + tracing::error!("Error encountered while running migrations: {}", e); } Ok(()) diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 00000000..e057f589 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,34 @@ +use tracing_subscriber::EnvFilter; + +/// Initialize the global `tracing` subscriber. +/// +/// The filter is controlled by the `RUST_LOG` env var, falling back to +/// `"info,sqlx=warn,tracing_actix_web=info"` when unset. This default is +/// identical for debug and release builds. +/// +/// The output format is controlled by the `LOG_FORMAT` env var: when it is +/// exactly `"json"`, logs are emitted as JSON; otherwise the default +/// compact/full text format is used. +pub fn init() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn,tracing_actix_web=info")); + + let is_json = std::env::var("LOG_FORMAT").map(|v| v == "json").unwrap_or(false); + + if is_json { + tracing_subscriber::fmt() + .with_file(true) + .with_line_number(true) + .with_target(true) + .json() + .with_env_filter(filter) + .init(); + } else { + tracing_subscriber::fmt() + .with_file(true) + .with_line_number(true) + .with_target(true) + .with_env_filter(filter) + .init(); + } +} diff --git a/src/main.rs b/src/main.rs index 0b939094..5b7845fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod endpoints; mod events; mod extractors; mod jobs; +mod logging; mod mod_zip; mod openapi; mod types; @@ -27,8 +28,7 @@ mod storage; #[tokio::main] async fn main() -> anyhow::Result<()> { - log4rs::init_file("config/log4rs.yaml", Default::default()) - .map_err(|e| e.context("Failed to read log4rs config"))?; + logging::init(); let app_data = config::build_config().await?; app_data.static_storage().init().await?; @@ -38,13 +38,13 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } - log::info!("Running migrations"); + tracing::info!("Running migrations"); sqlx::migrate!("./migrations").run(app_data.db()).await?; let port = app_data.port(); let debug = app_data.debug(); - log::info!("Starting server on 0.0.0.0:{}", port); + tracing::info!("Starting server on 0.0.0.0:{}", port); let server = HttpServer::new(move || { let openapi = ApiDoc::openapi(); @@ -125,7 +125,7 @@ async fn main() -> anyhow::Result<()> { .bind(("0.0.0.0", port))?; if debug { - log::info!("Running in debug mode, using 1 thread."); + tracing::info!("Running in debug mode, using 1 thread."); server.workers(1).run().await?; } else { server.run().await?; diff --git a/src/mod_zip.rs b/src/mod_zip.rs index 1da03001..8b089581 100644 --- a/src/mod_zip.rs +++ b/src/mod_zip.rs @@ -46,13 +46,13 @@ pub fn extract_mod_logo(file: &mut ZipFile>) -> Result, Mo let mut logo: Vec = Vec::with_capacity(file.size() as usize); file.read_to_end(&mut logo) - .inspect_err(|e| log::error!("logo.png read fail: {}", e))?; + .inspect_err(|e| tracing::error!("logo.png read fail: {}", e))?; let mut reader = BufReader::new(Cursor::new(logo)); let mut img = PngDecoder::new(&mut reader) .and_then(DynamicImage::from_decoder) - .inspect_err(|e| log::error!("Failed to create PngDecoder: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to create PngDecoder: {}", e))?; let dimensions = img.dimensions(); @@ -79,7 +79,7 @@ pub fn extract_mod_logo(file: &mut ZipFile>) -> Result, Mo encoder .write_image(img.as_bytes(), width, height, img.color().into()) - .inspect_err(|e| log::error!("Failed to downscale image to 336x336: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to downscale image to 336x336: {}", e))?; cursor.seek(std::io::SeekFrom::Start(0)).unwrap(); @@ -99,13 +99,13 @@ pub fn validate_mod_logo(file: &mut ZipFile>) -> Result<(), ModZip let mut logo: Vec = Vec::with_capacity(file.size() as usize); file.read_to_end(&mut logo) - .inspect_err(|e| log::error!("logo.png read fail: {}", e))?; + .inspect_err(|e| tracing::error!("logo.png read fail: {}", e))?; let mut reader = BufReader::new(Cursor::new(logo)); let img = PngDecoder::new(&mut reader) .and_then(DynamicImage::from_decoder) - .inspect_err(|e| log::error!("Failed to create PngDecoder: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to create PngDecoder: {}", e))?; let dimensions = img.dimensions(); @@ -142,7 +142,7 @@ pub async fn download_mod_hash_comp( pub fn bytes_to_ziparchive(bytes: Bytes) -> Result>, ModZipError> { ZipArchive::new(Cursor::new(bytes)) - .inspect_err(|e| log::error!("Failed to create ZipArchive: {}", e)) + .inspect_err(|e| tracing::error!("Failed to create ZipArchive: {}", e)) .map_err(|e| e.into()) } @@ -150,9 +150,9 @@ async fn download(url: &str, limit_mb: u32) -> Result { let limit_bytes: u64 = limit_mb as u64 * 1_000_000; let mut response = reqwest::get(url) .await - .inspect_err(|e| log::error!("Failed to fetch .geode file: {e}"))? + .inspect_err(|e| tracing::error!("Failed to fetch .geode file: {e}"))? .error_for_status() - .inspect_err(|e| log::error!("Failed to fetch .geode file: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to fetch .geode file: {e}"))?; // Check Content-Length, but the server can lie about this, so we'll also stream the file // If the header is somehow unavailable, we'll just check the size when streaming diff --git a/src/types/mod_json.rs b/src/types/mod_json.rs index 26cc02f9..29a8bbe7 100644 --- a/src/types/mod_json.rs +++ b/src/types/mod_json.rs @@ -217,7 +217,7 @@ impl ModJson { } let mut json = serde_json::from_reader::>, ModJson>(json_file) - .inspect_err(|e| log::error!("Failed to parse mod.json: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to parse mod.json: {e}"))?; json.version = json.version.trim_start_matches('v').to_string(); json.hash = hash; @@ -247,7 +247,7 @@ impl ModJson { json.about = Some( parse_zip_entry_to_str(&mut file) - .inspect_err(|e| log::error!("Failed to parse about.md for mod: {e}")) + .inspect_err(|e| tracing::error!("Failed to parse about.md for mod: {e}")) .map_err(|e| { ModZipError::InvalidModJson(format!("Failed to read about.md: {e}")) })?, @@ -262,7 +262,7 @@ impl ModJson { json.changelog = Some( parse_zip_entry_to_str(&mut file) - .inspect_err(|e| log::error!("Failed to parse changelog.md: {e}")) + .inspect_err(|e| tracing::error!("Failed to parse changelog.md: {e}")) .map_err(|e| { ModZipError::InvalidModJson(format!( "Failed to read changelog.md: {e}" @@ -483,7 +483,7 @@ impl ModJson { pub fn validate(&self) -> Result<(), ModZipError> { if let Err(e) = ::validate(self) { - log::warn!("mod.json validation error: {e}"); + tracing::warn!("mod.json validation error: {e}"); let useful_error = extract_validation_error(&e); return Err(ModZipError::InvalidModJson(format!( "validation error: {useful_error}" @@ -595,7 +595,7 @@ fn parse_zip_entry_to_str(file: &mut ZipFile>) -> Result Ok(string), Err(e) => { - log::error!("{}", e); + tracing::error!("{}", e); Err(format!("Failed to parse {}", file.name())) } } @@ -636,7 +636,7 @@ fn check_mac_binary(file: &mut ZipFile>) -> Result<(bool, bool), M // 12 bytes is all we need let mut bytes: Vec = vec![0; 12]; file.read_exact(&mut bytes).map_err(|e| { - log::error!("Failed to read MacOS binary: {}", e); + tracing::error!("Failed to read MacOS binary: {}", e); ModZipError::InvalidBinaries(format!("Failed to read macOS binary: {e}")) })?; diff --git a/src/types/models/dependency.rs b/src/types/models/dependency.rs index e00030c6..25a3ecaf 100644 --- a/src/types/models/dependency.rs +++ b/src/types/models/dependency.rs @@ -191,7 +191,7 @@ impl Dependency { .bind(geode_pre) .fetch_all(&mut *pool) .await - .inspect_err(|x| log::error!("Failed to fetch dependencies: {x}"))?; + .inspect_err(|x| tracing::error!("Failed to fetch dependencies: {x}"))?; let mut ret: HashMap> = HashMap::new(); for i in result { diff --git a/src/types/models/incompatibility.rs b/src/types/models/incompatibility.rs index 6ef49126..fbe57e9c 100644 --- a/src/types/models/incompatibility.rs +++ b/src/types/models/incompatibility.rs @@ -106,7 +106,7 @@ impl Incompatibility { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch incompatibilities for mod_version {id}: {e}")) + .inspect_err(|e| tracing::error!("Failed to fetch incompatibilities for mod_version {id}: {e}")) .map_err(|e| e.into()) } @@ -162,7 +162,7 @@ impl Incompatibility { .bind(geode_pre); let result = q.fetch_all(&mut *pool).await.inspect_err(|e| { - log::error!("Failed to fetch incompatibilities for mod_versions: {e}") + tracing::error!("Failed to fetch incompatibilities for mod_versions: {e}") })?; let mut ret: HashMap> = HashMap::new(); diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index 30bb58ad..18d036af 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -132,7 +132,7 @@ impl Mod { ) .fetch_optional(&mut *pool) .await - .inspect_err(|e| log::error!("failed to get mod stats: {}", e))?; + .inspect_err(|e| tracing::error!("failed to get mod stats: {}", e))?; if let Some((Some(total_count), Some(total_downloads))) = result.map(|o| (o.id_count, o.download_sum)) @@ -356,13 +356,13 @@ impl Mod { records_builder.push(" LIMIT ").push_bind(limit); records_builder.push(" OFFSET ").push_bind(offset); - // log::debug!("sql: {}", records_builder.sql()); + // tracing::debug!("sql: {}", records_builder.sql()); let records: Vec = records_builder .build_query_as() .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch mod index: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to fetch mod index: {}", e))?; let mut count_builder = sqlx::QueryBuilder::new("SELECT COUNT(DISTINCT m.id) "); @@ -372,7 +372,7 @@ impl Mod { .build_query_scalar() .fetch_optional(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch mod index count: {}", e))? + .inspect_err(|e| tracing::error!("Failed to fetch mod index count: {}", e))? .unwrap_or_default(); if records.is_empty() { @@ -529,7 +529,7 @@ impl Mod { ) .fetch_all(&mut *pool) .await - .inspect_err(|x| log::error!("Failed to fetch developer mods: {}", x))?; + .inspect_err(|x| tracing::error!("Failed to fetch developer mods: {}", x))?; if records.is_empty() { return Ok(vec![]); @@ -597,7 +597,7 @@ impl Mod { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("{}", e))?; + .inspect_err(|e| tracing::error!("{}", e))?; if records.is_empty() { return Ok(None); @@ -678,7 +678,7 @@ impl Mod { sqlx::query!("UPDATE mods SET featured = $1 WHERE id = $2", featured, id) .execute(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to update mod {id}: {e}")) + .inspect_err(|e| tracing::error!("Failed to update mod {id}: {e}")) .map_err(|e| e.into()) .map(|_| ()) } @@ -749,7 +749,7 @@ impl Mod { ) .fetch_all(&mut *pool) .await - .inspect_err(|x| log::error!("Failed to fetch mod updates: {}", x))?; + .inspect_err(|x| tracing::error!("Failed to fetch mod updates: {}", x))?; if result.is_empty() { return Ok(vec![]); diff --git a/src/types/models/mod_gd_version.rs b/src/types/models/mod_gd_version.rs index ded0b28d..a201182e 100644 --- a/src/types/models/mod_gd_version.rs +++ b/src/types/models/mod_gd_version.rs @@ -247,7 +247,7 @@ impl ModGDVersion { .fetch_all(&mut *pool) .await .inspect_err(|e| { - log::error!("Failed to fetch mod_gd_versions for mod_version {id}: {e}") + tracing::error!("Failed to fetch mod_gd_versions for mod_version {id}: {e}") })?; let mut ret = DetailedGDVersion { win: None, @@ -300,7 +300,7 @@ impl ModGDVersion { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch mod_gd_versions: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to fetch mod_gd_versions: {}", e))?; let mut ret: HashMap = HashMap::new(); for i in result { diff --git a/src/types/models/mod_link.rs b/src/types/models/mod_link.rs index eeed5f56..fdee90b3 100644 --- a/src/types/models/mod_link.rs +++ b/src/types/models/mod_link.rs @@ -28,7 +28,7 @@ impl ModLinks { ) .fetch_optional(pool) .await - .inspect_err(|e| log::error!("Failed to fetch mod links for mod {}. Error: {}", mod_id, e)) + .inspect_err(|e| tracing::error!("Failed to fetch mod links for mod {}. Error: {}", mod_id, e)) .map_err(|e| e.into()) } @@ -50,7 +50,7 @@ impl ModLinks { ) .fetch_all(pool) .await - .inspect_err(|e| log::error!("Failed to fetch mod links for multiple mods. Error: {}", e)) + .inspect_err(|e| tracing::error!("Failed to fetch mod links for multiple mods. Error: {}", e)) .map_err(|e| e.into()) } } diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index b66c7b77..586932ff 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -249,13 +249,13 @@ impl ModVersion { .build_query_as::() .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch index: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to fetch index: {e}"))?; let count: i64 = counter_q .build_query_scalar() .fetch_one(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch index count: {e}"))?; + .inspect_err(|e| tracing::error!("Failed to fetch index count: {e}"))?; if records.is_empty() { return Ok(PaginatedData { @@ -371,7 +371,7 @@ impl ModVersion { .bind(requires_patching) .fetch_all(&mut *pool) .await - .inspect_err(|x| log::error!("Failed to fetch latest versions for mods: {}", x)) + .inspect_err(|x| tracing::error!("Failed to fetch latest versions for mods: {}", x)) .map_err(|e| e.into()) .map(|result: Vec| { result.into_iter() @@ -403,7 +403,7 @@ impl ModVersion { ids ).fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch pending mod versions: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to fetch pending mod versions: {}", e))?; let mut ret: HashMap> = HashMap::new(); @@ -472,7 +472,7 @@ impl ModVersion { .build_query_as::() .fetch_optional(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch latest mod_version for mod {id}: {e}"))? + .inspect_err(|e| tracing::error!("Failed to fetch latest mod_version for mod {id}: {e}"))? .map(|v| v.into_mod_version()); let Some(mut version) = version else { @@ -532,7 +532,7 @@ impl ModVersion { ) .fetch_optional(&mut *pool) .await - .inspect_err(|e| log::error!("ModVersion::get_one failed: {e}"))? + .inspect_err(|e| tracing::error!("ModVersion::get_one failed: {e}"))? .map(|x| x.into_mod_version()); let Some(mut version) = result else { diff --git a/src/types/models/stats.rs b/src/types/models/stats.rs index 64b2593e..55b82156 100644 --- a/src/types/models/stats.rs +++ b/src/types/models/stats.rs @@ -68,7 +68,7 @@ impl Stats { ) .execute(&mut *pool) .await - .inspect_err(|e| log::error!("{}", e))?; + .inspect_err(|e| tracing::error!("{}", e))?; Ok(new.0) } @@ -82,7 +82,7 @@ impl Stats { .send() .await .inspect_err(|e| { - log::error!("Failed to request Geode release stats from GitHub: {}", e) + tracing::error!("Failed to request Geode release stats from GitHub: {}", e) })?; if !resp.status().is_success() { diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index 0904270c..1dccd0cc 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -26,7 +26,7 @@ impl Tag { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("{}", e)) + .inspect_err(|e| tracing::error!("{}", e)) .map(|tags| tags.into_iter().map(|t| t.name).collect::>()) .map_err(|e| e.into()) } @@ -43,7 +43,7 @@ impl Tag { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("{}", e))?; + .inspect_err(|e| tracing::error!("{}", e))?; let mut ret: HashMap> = HashMap::new(); for tag in tags { @@ -69,7 +69,7 @@ impl Tag { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch tags: {}", e))?; + .inspect_err(|e| tracing::error!("Failed to fetch tags: {}", e))?; let fetched_ids = fetched.iter().map(|t| t.id).collect::>(); let fetched_names = fetched diff --git a/src/webhook/discord.rs b/src/webhook/discord.rs index c5294581..f492bbf8 100644 --- a/src/webhook/discord.rs +++ b/src/webhook/discord.rs @@ -67,11 +67,11 @@ impl DiscordMessage { pub fn send(&self, url: &str) { if url.is_empty() { - log::error!("Not sending webhook since URL is empty"); + tracing::error!("Not sending webhook since URL is empty"); return; } - log::debug!("Sending {:?} to webhook url {}", self, url); + tracing::debug!("Sending {:?} to webhook url {}", self, url); let url = String::from(url); let copy = self.clone(); @@ -83,7 +83,7 @@ impl DiscordMessage { .send() .await { - log::error!("Failed to broadcast Discord webhook {}: {}", url, e); + tracing::error!("Failed to broadcast Discord webhook {}: {}", url, e); } }); } From 522a1fa39a6bcf2029c683fc1e53f1001c39655c Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 02:04:31 +0300 Subject: [PATCH 44/69] feat: replace actix logger with tracing --- src/endpoints/mod.rs | 3 +++ src/main.rs | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 32ad388e..c1e66e20 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -70,6 +70,9 @@ impl actix_web::ResponseError for ApiError { } fn error_response(&self) -> HttpResponse { + if self.status_code().is_server_error() { + tracing::error!(error = ?self, "request failed"); + } HttpResponse::build(self.status_code()).json(self.as_response()) } } diff --git a/src/main.rs b/src/main.rs index 5b7845fe..37753912 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use crate::types::api; use actix_cors::Cors; use actix_web::{ App, HttpServer, - middleware::Logger, web::{self, QueryConfig}, }; use utoipa::OpenApi; @@ -62,7 +61,7 @@ async fn main() -> anyhow::Result<()> { .supports_credentials() .max_age(3600), ) - .wrap(Logger::default()); + .wrap(tracing_actix_web::TracingLogger::default()); #[cfg(feature = "dev-tools")] let app = app From 8b5f712f639bde6c058d26f3fa7fa8c155522b5a Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 02:16:23 +0300 Subject: [PATCH 45/69] feat: add tracing::instrument to all handlers --- src/endpoints/auth/github.rs | 5 +++++ src/endpoints/auth/mod.rs | 1 + src/endpoints/deprecations.rs | 5 +++++ src/endpoints/developers.rs | 10 ++++++++++ src/endpoints/health.rs | 1 + src/endpoints/loader.rs | 3 +++ src/endpoints/mod_status_badge.rs | 1 + src/endpoints/mod_version_submissions.rs | 11 +++++++++++ src/endpoints/mod_versions.rs | 5 +++++ src/endpoints/mods.rs | 6 ++++++ src/endpoints/stats.rs | 1 + src/endpoints/tags.rs | 2 ++ 12 files changed, 51 insertions(+) diff --git a/src/endpoints/auth/github.rs b/src/endpoints/auth/github.rs index 687c5e67..484e4060 100644 --- a/src/endpoints/auth/github.rs +++ b/src/endpoints/auth/github.rs @@ -40,6 +40,7 @@ struct CallbackParams { ) )] #[post("v1/login/github")] +#[tracing::instrument(skip_all, err)] pub async fn start_github_login( data: web::Data, info: ConnectionInfo, @@ -77,6 +78,7 @@ pub async fn start_github_login( ) )] #[post("v1/login/github/web")] +#[tracing::instrument(skip_all, err)] pub async fn start_github_web_login(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; @@ -106,6 +108,7 @@ pub async fn start_github_web_login(data: web::Data) -> Result, data: web::Data, @@ -163,6 +166,7 @@ pub async fn github_web_callback( ) )] #[post("v1/login/github/poll")] +#[tracing::instrument(skip_all, err)] pub async fn poll_github_login( json: web::Json, data: web::Data, @@ -267,6 +271,7 @@ pub async fn poll_github_login( ) )] #[post("v1/login/github/token")] +#[tracing::instrument(skip_all, err)] pub async fn github_token_login( json: web::Json, data: web::Data, diff --git a/src/endpoints/auth/mod.rs b/src/endpoints/auth/mod.rs index bf72a8a9..ebfd5f35 100644 --- a/src/endpoints/auth/mod.rs +++ b/src/endpoints/auth/mod.rs @@ -34,6 +34,7 @@ struct RefreshBody { ) )] #[post("v1/login/refresh")] +#[tracing::instrument(skip_all, err)] pub async fn refresh_token( json: web::Json, data: web::Data, diff --git a/src/endpoints/deprecations.rs b/src/endpoints/deprecations.rs index f07fa348..af0c3da9 100644 --- a/src/endpoints/deprecations.rs +++ b/src/endpoints/deprecations.rs @@ -48,6 +48,7 @@ const MAX_MODS_PER_DEPRECATION: usize = 20; ) )] #[get("v1/mods/{id}/deprecations")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] pub async fn index( data: web::Data, path: web::Path @@ -84,6 +85,7 @@ pub async fn index( ) )] #[post("v1/mods/{id}/deprecations")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] pub async fn store( data: web::Data, path: web::Path, @@ -131,6 +133,7 @@ pub async fn store( ) )] #[put("v1/mods/{id}/deprecations/{deprecation_id}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] pub async fn update( data: web::Data, path: web::Path, @@ -201,6 +204,7 @@ pub async fn update( ) )] #[delete("v1/mods/{id}/deprecations/{deprecation_id}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] pub async fn delete( data: web::Data, path: web::Path, @@ -254,6 +258,7 @@ pub async fn delete( ) )] #[delete("v1/mods/{id}/deprecations")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] pub async fn clear_all( data: web::Data, path: web::Path, diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index b884fe75..d97bc4f9 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -82,6 +82,7 @@ struct DeveloperIndexQuery { ) )] #[get("v1/developers")] +#[tracing::instrument(skip_all, err)] pub async fn developer_index( data: web::Data, query: web::Query, @@ -118,6 +119,7 @@ pub async fn developer_index( ) )] #[post("v1/mods/{id}/developers")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, username = %json.username))] pub async fn add_developer_to_mod( data: web::Data, path: web::Path, @@ -164,6 +166,7 @@ pub async fn add_developer_to_mod( ) )] #[delete("v1/mods/{id}/developers/{username}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, username = %path.username))] pub async fn remove_dev_from_mod( data: web::Data, path: web::Path, @@ -220,6 +223,7 @@ pub async fn remove_dev_from_mod( ) )] #[delete("v1/me/token")] +#[tracing::instrument(skip_all, err)] pub async fn delete_token( data: web::Data, auth: Auth, @@ -246,6 +250,7 @@ pub async fn delete_token( ) )] #[delete("v1/me/tokens")] +#[tracing::instrument(skip_all, err)] pub async fn delete_tokens( data: web::Data, auth: Auth, @@ -280,6 +285,7 @@ struct UploadProfilePayload { ) )] #[put("v1/me")] +#[tracing::instrument(skip_all, err)] pub async fn update_profile( data: web::Data, json: web::Json, @@ -337,6 +343,7 @@ pub fn default_own_mods_status() -> ModVersionStatusEnum { ) )] #[get("v1/me/mods")] +#[tracing::instrument(skip_all, err)] pub async fn get_own_mods( data: web::Data, query: web::Query, @@ -366,6 +373,7 @@ pub async fn get_own_mods( ) )] #[get("v1/me")] +#[tracing::instrument(skip_all, err)] pub async fn get_me(auth: Auth, data: web::Data) -> Result { let mut pool = data.db().acquire().await?; let dev = auth.developer()?; @@ -395,6 +403,7 @@ struct GetDeveloperPath { ) )] #[get("v1/developers/{id}")] +#[tracing::instrument(skip_all, err, fields(developer_id = %path.id))] pub async fn get_developer( data: web::Data, path: web::Path, @@ -429,6 +438,7 @@ pub async fn get_developer( ) )] #[put("v1/developers/{id}")] +#[tracing::instrument(skip_all, err, fields(developer_id = %path.id))] pub async fn update_developer( auth: Auth, data: web::Data, diff --git a/src/endpoints/health.rs b/src/endpoints/health.rs index cbd22216..b97aec9f 100644 --- a/src/endpoints/health.rs +++ b/src/endpoints/health.rs @@ -10,6 +10,7 @@ use actix_web::route; ) )] #[route("/", method = "GET", method = "HEAD")] +#[tracing::instrument(skip_all)] pub async fn health() -> &'static str { r#" _____ _____ diff --git a/src/endpoints/loader.rs b/src/endpoints/loader.rs index ef875ef0..ce8df27b 100644 --- a/src/endpoints/loader.rs +++ b/src/endpoints/loader.rs @@ -44,6 +44,7 @@ struct GetOnePath { ) )] #[get("v1/loader/versions/{version}")] +#[tracing::instrument(skip_all, err, fields(version = %path.version))] pub async fn get_one( path: web::Path, data: web::Data, @@ -108,6 +109,7 @@ struct CreateVersionBody { ) )] #[post("v1/loader/versions")] +#[tracing::instrument(skip_all, err, fields(tag = %payload.tag))] pub async fn create_version( data: web::Data, payload: web::Json, @@ -160,6 +162,7 @@ struct GetManyQuery { ) )] #[get("v1/loader/versions")] +#[tracing::instrument(skip_all, err)] pub async fn get_many( data: web::Data, query: web::Query, diff --git a/src/endpoints/mod_status_badge.rs b/src/endpoints/mod_status_badge.rs index 7183e971..76938efd 100644 --- a/src/endpoints/mod_status_badge.rs +++ b/src/endpoints/mod_status_badge.rs @@ -39,6 +39,7 @@ pub struct StatusBadgeQuery { ) )] #[get("/v1/mods/{id}/status_badge")] +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn status_badge( data: web::Data, id: web::Path, diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index a935567b..75652d84 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -105,6 +105,7 @@ async fn check_submission_lock( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn get_submission( path: web::Path, data: web::Data @@ -158,6 +159,7 @@ pub async fn get_submission( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/audit")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn get_submission_audit( path: web::Path, data: web::Data, @@ -202,6 +204,7 @@ pub async fn get_submission_audit( security(("bearer_token" = [])) )] #[put("v1/mods/{id}/versions/{version}/submission")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn update_submission( path: web::Path, data: web::Data, @@ -260,6 +263,7 @@ pub async fn update_submission( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/comments")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn get_comments( path: web::Path, data: web::Data, @@ -354,6 +358,7 @@ pub async fn get_comments( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/audit")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn get_comment_audit( path: web::Path, data: web::Data, @@ -405,6 +410,7 @@ pub async fn get_comment_audit( security(("bearer_token" = [])) )] #[post("v1/mods/{id}/versions/{version}/submission/comments")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn create_comment( path: web::Path, data: web::Data, @@ -487,6 +493,7 @@ pub async fn create_comment( security(("bearer_token" = [])) )] #[put("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn update_comment( path: web::Path, data: web::Data, @@ -586,6 +593,7 @@ pub async fn update_comment( security(("bearer_token" = [])) )] #[delete("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn delete_comment( path: web::Path, data: web::Data, @@ -671,6 +679,7 @@ pub async fn delete_comment( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn get_attachments( path: web::Path, data: web::Data @@ -741,6 +750,7 @@ pub struct UploadAttachmentsForm { security(("bearer_token" = [])) )] #[post("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn upload_attachments( path: web::Path, data: web::Data, @@ -910,6 +920,7 @@ pub async fn upload_attachments( #[delete( "v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments/{attachment_id}" )] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id, attachment_id = %path.attachment_id))] pub async fn delete_attachment( path: web::Path, data: web::Data, diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 52383879..e1a72d10 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -89,6 +89,7 @@ struct IndexQuery { ) )] #[get("v1/mods/{id}/versions")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] pub async fn get_version_index( path: web::Path, data: web::Data, @@ -149,6 +150,7 @@ pub async fn get_version_index( ) )] #[get("v1/mods/{id}/versions/{version}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn get_one( path: web::Path, data: web::Data, @@ -211,6 +213,7 @@ struct DownloadQuery { ) )] #[get("v1/mods/{id}/versions/{version}/download")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn download_version( path: web::Path, data: web::Data, @@ -292,6 +295,7 @@ pub async fn download_version( ) )] #[post("v1/mods/{id}/versions")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path))] pub async fn create_version( path: web::Path, data: web::Data, @@ -497,6 +501,7 @@ pub async fn create_version( ) )] #[put("v1/mods/{id}/versions/{version}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] pub async fn update_version( path: web::Path, data: web::Data, diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index 875dc710..50ac5661 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -80,6 +80,7 @@ pub struct CreateQueryParams { ) )] #[get("/v1/mods")] +#[tracing::instrument(skip_all, err)] pub async fn index( data: web::Data, query: web::Query, @@ -127,6 +128,7 @@ pub async fn index( ) )] #[get("/v1/mods/{id}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn get( data: web::Data, id: web::Path, @@ -201,6 +203,7 @@ pub async fn get( ) )] #[post("/v1/mods")] +#[tracing::instrument(skip_all, err)] pub async fn create( data: web::Data, payload: web::Json, @@ -334,6 +337,7 @@ enum UpdateQueryResponse { ) )] #[get("/v1/mods/updates")] +#[tracing::instrument(skip_all, err, fields(ids = %query.ids, geode = %query.geode))] pub async fn get_mod_updates( data: web::Data, query: web::Query, @@ -394,6 +398,7 @@ pub async fn get_mod_updates( ) )] #[get("/v1/mods/{id}/logo")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path))] pub async fn get_logo( data: web::Data, path: web::Path, @@ -433,6 +438,7 @@ struct UpdateModPayload { ) )] #[put("/v1/mods/{id}")] +#[tracing::instrument(skip_all, err, fields(mod_id = %path))] pub async fn update_mod( data: web::Data, path: web::Path, diff --git a/src/endpoints/stats.rs b/src/endpoints/stats.rs index 22ecaf01..982e0103 100644 --- a/src/endpoints/stats.rs +++ b/src/endpoints/stats.rs @@ -14,6 +14,7 @@ use crate::types::{api::ApiResponse, models::stats::Stats}; ) )] #[get("/v1/stats")] +#[tracing::instrument(skip_all, err)] pub async fn get_stats(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; Ok(web::Json(ApiResponse { diff --git a/src/endpoints/tags.rs b/src/endpoints/tags.rs index 25bd0bfc..0d1c76e9 100644 --- a/src/endpoints/tags.rs +++ b/src/endpoints/tags.rs @@ -16,6 +16,7 @@ use crate::types::models::tag::Tag; ) )] #[get("/v1/tags")] +#[tracing::instrument(skip_all, err)] pub async fn index(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; let tags = mod_tags::get_all_writable(&mut pool) @@ -40,6 +41,7 @@ pub async fn index(data: web::Data) -> Result ) )] #[get("/v1/detailed-tags")] +#[tracing::instrument(skip_all, err)] pub async fn detailed_index(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; From 1ddca14145fc2ec8acb0cf39d744eda4144a5e99 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 02:35:51 +0300 Subject: [PATCH 46/69] feat: implement tracing::instrument for DB layer --- src/database/repository/auth_tokens.rs | 18 +++--- src/database/repository/dependencies.rs | 4 +- src/database/repository/deprecations.rs | 35 +++++----- src/database/repository/developers.rs | 58 +++++++---------- .../repository/github_login_attempts.rs | 14 ++-- src/database/repository/github_web_logins.rs | 10 +-- src/database/repository/incompatibilities.rs | 4 +- src/database/repository/mod_downloads.rs | 12 ++-- src/database/repository/mod_gd_versions.rs | 6 +- src/database/repository/mod_links.rs | 4 +- src/database/repository/mod_tags.rs | 21 +++--- .../repository/mod_version_statuses.rs | 2 +- .../repository/mod_version_submissions.rs | 64 ++++++++----------- src/database/repository/mod_versions.rs | 40 +++++------- src/database/repository/mods.rs | 51 ++++++--------- src/database/repository/refresh_tokens.rs | 16 ++--- src/types/models/dependency.rs | 4 +- src/types/models/gd_version_alias.rs | 1 + src/types/models/incompatibility.rs | 7 +- src/types/models/loader_version.rs | 4 ++ src/types/models/mod_entity.rs | 25 ++++---- src/types/models/mod_gd_version.rs | 10 ++- src/types/models/mod_link.rs | 4 +- src/types/models/mod_version.rs | 22 +++---- src/types/models/stats.rs | 5 +- src/types/models/tag.rs | 10 +-- 26 files changed, 199 insertions(+), 252 deletions(-) diff --git a/src/database/repository/auth_tokens.rs b/src/database/repository/auth_tokens.rs index e3953aaf..2d8c2fd9 100644 --- a/src/database/repository/auth_tokens.rs +++ b/src/database/repository/auth_tokens.rs @@ -4,6 +4,7 @@ use sqlx::PgConnection; use uuid::Uuid; /// Assumes developer ID exists +#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] pub async fn generate_token( developer_id: i32, with_expiry: bool, @@ -27,14 +28,12 @@ pub async fn generate_token( expiry ) .execute(&mut *conn) - .await - .inspect_err(|e| { - tracing::error!("Failed to insert auth_token for developer {developer_id}: {e}") - })?; + .await?; Ok(token) } +#[tracing::instrument(skip_all, err)] pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let hash = sha256::digest(token.to_string()); @@ -44,12 +43,12 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da hash ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to remove auth token: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] pub async fn remove_developer_tokens( developer_id: i32, conn: &mut PgConnection, @@ -60,20 +59,19 @@ pub async fn remove_developer_tokens( developer_id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to wipe developer tokens: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err)] pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM auth_tokens WHERE expires_at < NOW()" ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Auth token cleanup failed: {e}"))?; + .await?; Ok(()) } diff --git a/src/database/repository/dependencies.rs b/src/database/repository/dependencies.rs index 8b91544b..ce1c6104 100644 --- a/src/database/repository/dependencies.rs +++ b/src/database/repository/dependencies.rs @@ -8,6 +8,7 @@ use crate::{ }, }; +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, json: &ModJson, @@ -59,10 +60,10 @@ pub async fn create( ) .fetch_all(conn) .await - .inspect_err(|e| tracing::error!("dependenceis::create query failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM dependencies @@ -71,7 +72,6 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError ) .execute(conn) .await - .inspect_err(|e| tracing::error!("dependencies::clear query failed: {e}")) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/deprecations.rs b/src/database/repository/deprecations.rs index 2d87e72c..273f2e6c 100644 --- a/src/database/repository/deprecations.rs +++ b/src/database/repository/deprecations.rs @@ -3,6 +3,7 @@ use crate::types::models::deprecations::Deprecation; use crate::types::models::developer::Developer; use sqlx::PgConnection; +#[tracing::instrument(skip_all, err, fields(mod_ids = ?ids))] pub async fn get_for_mods( ids: &[String], conn: &mut PgConnection, @@ -14,8 +15,7 @@ pub async fn get_for_mods( ids ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::get_for_mods failed: {e}"))?; + .await?; let mut bys: Vec<_> = sqlx::query!( "SELECT dby.deprecation_id, dby.by_mod_id @@ -24,8 +24,7 @@ pub async fn get_for_mods( &deps.iter().map(|d| d.id).collect::>() ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::get_for_mods failed: {e}"))?; + .await?; Ok(deps .into_iter() @@ -41,6 +40,7 @@ pub async fn get_for_mods( .collect()) } +#[tracing::instrument(skip_all, err, fields(deprecation_id = %id))] pub async fn get(id: i32, conn: &mut PgConnection) -> Result, DatabaseError> { let dep = sqlx::query!( "SELECT @@ -52,8 +52,7 @@ pub async fn get(id: i32, conn: &mut PgConnection) -> Result id ) .fetch_optional(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::get failed: {e}"))? + .await? .map(|x| Deprecation { id, mod_id: x.mod_id, @@ -74,14 +73,14 @@ pub async fn get(id: i32, conn: &mut PgConnection) -> Result dep.id ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::get failed: {e}"))?; + .await?; dep.by = deprecated_by.into_iter().map(|b| b.by_mod_id).collect(); Ok(Some(dep)) } +#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] pub async fn create( mod_id: &str, by: &[String], @@ -98,8 +97,7 @@ pub async fn create( updated_by.id ) .fetch_one(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::create failed: {e}"))? + .await? .id; if !by.is_empty() { @@ -114,6 +112,7 @@ pub async fn create( }) } +#[tracing::instrument(skip_all, err, fields(deprecation_id = %deprecation.id))] pub async fn update( mut deprecation: Deprecation, by: Option<&[String]>, @@ -139,8 +138,7 @@ pub async fn update( deprecation.id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::update failed: {e}"))?; + .await?; deprecation.reason = reason.to_string(); @@ -154,8 +152,7 @@ pub async fn update( deprecation.id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::update failed: {e}"))?; + .await?; insert_deprecated_by(deprecation.id, by, &mut *conn).await?; @@ -178,6 +175,7 @@ pub async fn update( Ok(deprecation) } +#[tracing::instrument(skip_all, err, fields(deprecation_id = %id))] pub async fn delete(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM deprecations @@ -185,12 +183,12 @@ pub async fn delete(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseErro id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::delete failed: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] pub async fn clear_all(mod_id: &str, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM deprecations @@ -198,12 +196,12 @@ pub async fn clear_all(mod_id: &str, conn: &mut PgConnection) -> Result<(), Data mod_id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("deprecations::clear_all failed: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err, fields(deprecation_id = %id))] async fn insert_deprecated_by( id: i32, by: &[String], @@ -227,7 +225,6 @@ async fn insert_deprecated_by( ) .execute(&mut *conn) .await - .inspect_err(|e| tracing::error!("deprecations::insert_deprecated_by failed: {e}")) .map(|_| ()) .map_err(|e| e.into()) } diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index f60271be..fef7fa59 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -5,6 +5,7 @@ use sqlx::PgConnection; use std::collections::HashMap; use uuid::Uuid; +#[tracing::instrument(skip_all, err, fields(query = ?query))] pub async fn index( query: Option<&str>, page: i64, @@ -36,8 +37,7 @@ pub async fn index( offset ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to fetch developers: {}", e))?; + .await?; let count = index_count(query, &mut *conn).await?; @@ -47,6 +47,7 @@ pub async fn index( }) } +#[tracing::instrument(skip_all, err, fields(query = ?query))] pub async fn index_count( query: Option<&str>, conn: &mut PgConnection, @@ -62,11 +63,11 @@ pub async fn index_count( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| tracing::error!("Failed to fetch developer count: {}", e)) .map(|x| x.count.unwrap_or(0)) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(github_id = %github_id, username = %username))] pub async fn fetch_or_insert_github( github_id: i64, username: &str, @@ -86,14 +87,14 @@ pub async fn fetch_or_insert_github( github_id ) .fetch_optional(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to fetch developer for GitHub id: {e}"))? + .await? { Some(dev) => Ok(dev), None => Ok(insert_github(github_id, username, conn).await?), } } +#[tracing::instrument(skip_all, err, fields(github_id = %github_id, username = %username))] async fn insert_github( github_id: i64, username: &str, @@ -115,10 +116,10 @@ async fn insert_github( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| tracing::error!("Failed to insert developer: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(developer_id = %id))] pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result, DatabaseError> { sqlx::query_as!( Developer, @@ -135,10 +136,10 @@ pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result> = HashMap::new(); @@ -267,6 +267,7 @@ pub async fn get_all_for_mods( Ok(ret) } +#[tracing::instrument(skip_all, err, fields(developer_id = %dev_id, mod_id = %mod_id))] pub async fn has_access_to_mod( dev_id: i32, mod_id: &str, @@ -281,18 +282,11 @@ pub async fn has_access_to_mod( ) .fetch_optional(&mut *conn) .await - .inspect_err(|e| { - tracing::error!( - "Failed to find mod {} access for developer {}: {}", - mod_id, - dev_id, - e - ); - }) .map(|x| x.is_some()) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(developer_id = %dev_id))] pub async fn has_active_mod(dev_id: i32, conn: &mut PgConnection) -> Result { sqlx::query!( "SELECT mods.id FROM mods @@ -306,11 +300,11 @@ pub async fn has_active_mod(dev_id: i32, conn: &mut PgConnection) -> Result Result { sqlx::query!( "SELECT mod_versions.id @@ -486,6 +473,5 @@ pub async fn has_accepted_mod(id: i32, conn: &mut PgConnection) -> Result Result<(), DatabaseError> { let now = Utc::now(); sqlx::query!( @@ -104,17 +105,16 @@ pub async fn poll_now(uuid: Uuid, conn: &mut PgConnection) -> Result<(), Databas uuid ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to poll GitHub login attempt: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err)] pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_login_attempts WHERE uid = $1", uuid) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to remove GitHub login attempt: {e}"))?; + .await?; Ok(()) } diff --git a/src/database/repository/github_web_logins.rs b/src/database/repository/github_web_logins.rs index 9c0e0e99..fc341ec5 100644 --- a/src/database/repository/github_web_logins.rs +++ b/src/database/repository/github_web_logins.rs @@ -2,31 +2,31 @@ use crate::database::DatabaseError; use sqlx::PgConnection; use uuid::Uuid; +#[tracing::instrument(skip_all, err)] pub async fn create_unique(conn: &mut PgConnection) -> Result { let unique = Uuid::new_v4(); sqlx::query!("INSERT INTO github_web_logins (state) VALUES ($1)", unique) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to create GitHub web login secret: {e}"))?; + .await?; Ok(unique) } +#[tracing::instrument(skip_all, err)] pub async fn exists(uuid: Uuid, conn: &mut PgConnection) -> Result { sqlx::query!("SELECT state FROM github_web_logins WHERE state = $1", uuid) .fetch_optional(conn) .await - .inspect_err(|e| tracing::error!("Failed to delete GitHub web login secret: {e}")) .map(|x| x.is_some()) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err)] pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_web_logins WHERE state = $1", uuid) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to delete GitHub web login secret: {e}"))?; + .await?; Ok(()) } diff --git a/src/database/repository/incompatibilities.rs b/src/database/repository/incompatibilities.rs index 8e415d07..c4de1ee8 100644 --- a/src/database/repository/incompatibilities.rs +++ b/src/database/repository/incompatibilities.rs @@ -11,6 +11,7 @@ use crate::{ }, }; +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, json: &ModJson, @@ -64,10 +65,10 @@ pub async fn create( ) .fetch_all(conn) .await - .inspect_err(|e| tracing::error!("incompatibilities::create query failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM incompatibilities @@ -76,7 +77,6 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError ) .execute(conn) .await - .inspect_err(|e| tracing::error!("incompatibilities::clear query failed: {e}")) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/mod_downloads.rs b/src/database/repository/mod_downloads.rs index 8a93477f..a09d2678 100644 --- a/src/database/repository/mod_downloads.rs +++ b/src/database/repository/mod_downloads.rs @@ -3,6 +3,7 @@ use chrono::{Days, Utc}; use sqlx::types::ipnetwork::IpNetwork; use sqlx::PgConnection; +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] pub async fn create( ip: IpNetwork, mod_version_id: i32, @@ -16,14 +17,12 @@ pub async fn create( ip ) .execute(&mut *conn) - .await - .inspect_err(|e| { - tracing::error!("Failed to insert new download for mod_version id {mod_version_id}: {e}"); - })?; + .await?; Ok(result.rows_affected() > 0) } +#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] pub async fn has_downloaded_mod( ip: IpNetwork, mod_id: &str, @@ -40,11 +39,11 @@ pub async fn has_downloaded_mod( ) .fetch_optional(&mut *conn) .await - .inspect_err(|e| tracing::error!("mod_downloads::has_downloaded_mod query error: {e}")) .map_err(|e| e.into()) .map(|x| x.is_some()) } +#[tracing::instrument(skip_all, err)] pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { let date = Utc::now().checked_sub_days(Days::new(30)).unwrap(); sqlx::query!( @@ -53,8 +52,7 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { date ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("mod_downloads::cleanup query failed: {e}"))?; + .await?; Ok(()) } diff --git a/src/database/repository/mod_gd_versions.rs b/src/database/repository/mod_gd_versions.rs index 8d49bcb0..a8100358 100644 --- a/src/database/repository/mod_gd_versions.rs +++ b/src/database/repository/mod_gd_versions.rs @@ -8,6 +8,7 @@ use crate::{ }, }; +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, json: &ModJson, @@ -32,12 +33,12 @@ pub async fn create( &mod_id ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("mod_gd_versions::create query failed: {e}"))?; + .await?; Ok(json.gd.clone()) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM mod_gd_versions mgv @@ -46,7 +47,6 @@ pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), D ) .execute(&mut *conn) .await - .inspect_err(|e| tracing::error!("incompatibilities::clear query failed: {e}")) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/mod_links.rs b/src/database/repository/mod_links.rs index 8776cf69..42a88437 100644 --- a/src/database/repository/mod_links.rs +++ b/src/database/repository/mod_links.rs @@ -2,6 +2,7 @@ use sqlx::PgConnection; use crate::{database::DatabaseError, types::models::mod_link::ModLinks}; +#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] pub async fn upsert( mod_id: &str, community: Option, @@ -25,8 +26,7 @@ pub async fn upsert( source ) .execute(&mut *conn) - .await - .inspect_err(|x| tracing::error!("Failed to upsert mod_links for id {mod_id}: {x}"))?; + .await?; Ok(ModLinks { mod_id: mod_id.into(), diff --git a/src/database/repository/mod_tags.rs b/src/database/repository/mod_tags.rs index 068332c7..5a095e81 100644 --- a/src/database/repository/mod_tags.rs +++ b/src/database/repository/mod_tags.rs @@ -3,6 +3,7 @@ use crate::database::DatabaseError; use crate::types::models::tag::Tag; use sqlx::PgConnection; +#[tracing::instrument(skip_all, err)] pub async fn get_all_writable(conn: &mut PgConnection) -> Result, DatabaseError> { let tags = sqlx::query!( "SELECT @@ -14,8 +15,7 @@ pub async fn get_all_writable(conn: &mut PgConnection) -> Result, Datab where is_readonly = false" ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("mod_tags::get_all_writeable failed: {e}"))? + .await? .into_iter() .map(|i| Tag { id: i.id, @@ -28,6 +28,7 @@ pub async fn get_all_writable(conn: &mut PgConnection) -> Result, Datab Ok(tags) } +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn get_allowed_for_mod( id: &str, conn: &mut PgConnection, @@ -46,8 +47,7 @@ pub async fn get_allowed_for_mod( id ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("mod_tags::get_allowed_for_mod failed: {e}"))? + .await? .into_iter() .map(|i| Tag { id: i.id, @@ -62,6 +62,7 @@ pub async fn get_allowed_for_mod( return Ok(writable); } +#[tracing::instrument(skip_all, err)] pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> { let tags = sqlx::query!( "SELECT @@ -72,8 +73,7 @@ pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> FROM mod_tags" ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("mod_tags::get_all failed: {e}"))? + .await? .into_iter() .map(|i| Tag { id: i.id, @@ -86,6 +86,7 @@ pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> Ok(tags) } +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, DatabaseError> { sqlx::query!( "SELECT @@ -100,7 +101,6 @@ pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, ) .fetch_all(&mut *conn) .await - .inspect_err(|e| tracing::error!("mod_tags::get_tags failed: {e}")) .map_err(|e| e.into()) .map(|vec| { vec.into_iter() @@ -114,6 +114,7 @@ pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, }) } +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn update_for_mod( id: &str, tags: &[Tag], @@ -144,8 +145,7 @@ pub async fn update_for_mod( &deletable ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to remove tags: {e}"))?; + .await?; } if !insertable.is_empty() { @@ -162,8 +162,7 @@ pub async fn update_for_mod( &insertable ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to insert tags: {e}"))?; + .await?; } Ok(()) diff --git a/src/database/repository/mod_version_statuses.rs b/src/database/repository/mod_version_statuses.rs index a9c0e4b5..c536a032 100644 --- a/src/database/repository/mod_version_statuses.rs +++ b/src/database/repository/mod_version_statuses.rs @@ -2,6 +2,7 @@ use crate::database::DatabaseError; use crate::types::models::mod_version_status::ModVersionStatusEnum; use sqlx::PgConnection; +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id, status = ?status))] pub async fn create( mod_version_id: i32, status: ModVersionStatusEnum, @@ -19,7 +20,6 @@ pub async fn create( ) .fetch_one(conn) .await - .inspect_err(|e| tracing::error!("Failed to create mod_version_status: {e}")) .map(|i| i.id) .map_err(|e| e.into()) } diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index 54b4931e..fa11c819 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -7,6 +7,7 @@ use crate::types::models::mod_version_submission::{ use sqlx::{Error, PgConnection}; use std::collections::HashMap; +#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] pub async fn get_for_mod_version( id: i32, conn: &mut PgConnection, @@ -22,10 +23,10 @@ pub async fn get_for_mod_version( ) .fetch_optional(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::get_for_mod_versions failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(submission_id = %id))] pub async fn get_audit_for_submission( id: i32, conn: &mut PgConnection, @@ -40,10 +41,10 @@ pub async fn get_audit_for_submission( ) .fetch_all(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::get_audit_for_submission failed: {e}",)) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, conn: &mut PgConnection, @@ -56,13 +57,13 @@ pub async fn create( mod_version_id ) .fetch_one(&mut *conn) - .await - .inspect_err(|e| tracing::error!("mod_version_submissions::create failed: {e}"))?; + .await?; insert_submission_audit(mod_version_id, AuditAction::Created, None, None, conn).await?; Ok(row) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id, lock = ?lock))] pub async fn set_locked( mod_version_id: i32, lock: ModVersionSubmissionLock, @@ -102,10 +103,10 @@ pub async fn set_locked( ) .fetch_one(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::set_locked failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(submission_id = %id))] pub async fn get_paginated_comments_for_submission( id: i32, page: i64, @@ -127,12 +128,10 @@ pub async fn get_paginated_comments_for_submission( ) .fetch_all(conn) .await - .inspect_err(|e| { - tracing::error!("mod_version_submissions::get_paginated_items_for_submission failed: {e}") - }) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(submission_id = %id))] pub async fn count_comments_for_submission( id: i32, conn: &mut PgConnection, @@ -143,13 +142,11 @@ pub async fn count_comments_for_submission( ) .fetch_one(conn) .await - .inspect_err(|e| { - tracing::error!("mod_version_submissions::count_comments_for_submission failed: {e}") - }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(submission_id = %submission_id, author_id = %author_id))] pub async fn create_comment( submission_id: i32, author_id: i32, @@ -167,10 +164,10 @@ pub async fn create_comment( ) .fetch_one(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::create_comment failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] pub async fn get_comment( comment_id: i64, conn: &mut PgConnection, @@ -184,10 +181,10 @@ pub async fn get_comment( ) .fetch_optional(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::get_comment failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] pub async fn update_comment( comment_id: i64, new_text: &str, @@ -204,10 +201,10 @@ pub async fn update_comment( ) .fetch_one(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::update_comment failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] pub async fn delete_comment( comment_id: i64, conn: &mut PgConnection, @@ -217,11 +214,11 @@ pub async fn delete_comment( comment_id ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("mod_version_submissions::delete_comment failed: {e}"))?; + .await?; Ok(result.rows_affected() > 0) } +#[tracing::instrument(skip_all, err, fields(comment_id = %id))] pub async fn get_audit_for_comment( id: i64, conn: &mut PgConnection, @@ -236,10 +233,10 @@ pub async fn get_audit_for_comment( ) .fetch_all(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::get_audit_for_comment failed: {e}",)) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] pub async fn count_attachments_for_comment( comment_id: i64, conn: &mut PgConnection, @@ -250,13 +247,11 @@ pub async fn count_attachments_for_comment( ) .fetch_one(conn) .await - .inspect_err(|e| { - tracing::error!("mod_version_submissions::count_attachments_for_comment failed: {e}") - }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id, filename = %filename))] pub async fn create_attachment( comment_id: i64, filename: &str, @@ -272,10 +267,10 @@ pub async fn create_attachment( ) .fetch_one(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::create_attachment failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] pub async fn get_attachments_for_comment( comment_id: i64, conn: &mut PgConnection, @@ -290,12 +285,10 @@ pub async fn get_attachments_for_comment( ) .fetch_all(conn) .await - .inspect_err(|e| { - tracing::error!("mod_version_submissions::get_attachments_for_comment failed: {e}") - }) .map_err(|e: Error| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_ids = ?comment_ids))] pub async fn get_attachments_for_comments( comment_ids: &[i64], conn: &mut PgConnection, @@ -309,10 +302,7 @@ pub async fn get_attachments_for_comments( comment_ids ) .fetch_all(conn) - .await - .inspect_err(|e| { - tracing::error!("mod_version_submissions::get_attachments_for_comments failed: {e}") - })?; + .await?; let mut ret: HashMap> = HashMap::with_capacity(comment_ids.len()); @@ -323,6 +313,7 @@ pub async fn get_attachments_for_comments( Ok(ret) } +#[tracing::instrument(skip_all, err, fields(attachment_id = %attachment_id))] pub async fn get_attachment( attachment_id: i64, conn: &mut PgConnection, @@ -336,10 +327,10 @@ pub async fn get_attachment( ) .fetch_optional(conn) .await - .inspect_err(|e| tracing::error!("mod_version_submissions::get_attachment failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(attachment_id = %attachment_id))] pub async fn delete_attachment( attachment_id: i64, conn: &mut PgConnection, @@ -349,11 +340,11 @@ pub async fn delete_attachment( attachment_id ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("mod_version_submissions::delete_attachment failed: {e}"))?; + .await?; Ok(result.rows_affected() > 0) } +#[tracing::instrument(skip_all, err, fields(filename = %filename))] pub async fn count_references_to_filename( filename: &str, conn: &mut PgConnection, @@ -364,13 +355,11 @@ pub async fn count_references_to_filename( ) .fetch_one(conn) .await - .inspect_err(|e| { - tracing::error!("mod_version_submissions::count_references_to_filename failed: {e}") - }) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(filenames = ?filenames))] pub async fn count_references_to_filenames( filenames: &[String], conn: &mut PgConnection, @@ -390,12 +379,10 @@ pub async fn count_references_to_filenames( .map(|record| (record.filename, record.count.unwrap_or(0))) .collect() }) - .inspect_err(|e| { - tracing::error!("mod_version_submissions::count_references_to_filenames failed: {e}") - }) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(submission_id = %id, action = ?action))] pub async fn insert_submission_audit( id: i32, action: AuditAction, @@ -414,10 +401,10 @@ pub async fn insert_submission_audit( .execute(conn) .await .map(|_| ()) - .inspect_err(|e| tracing::error!("mod_version_submissions::insert_submission_audit failed: {e}")) .map_err(|e| e.into()) } +#[tracing::instrument(skip_all, err, fields(comment_id = %id, action = ?action))] pub async fn insert_comment_audit( id: i64, action: AuditAction, @@ -436,6 +423,5 @@ pub async fn insert_comment_audit( .execute(conn) .await .map(|_| ()) - .inspect_err(|e| tracing::error!("mod_version_submissions::insert_comment_audit failed: {e}")) .map_err(|e| e.into()) } diff --git a/src/database/repository/mod_versions.rs b/src/database/repository/mod_versions.rs index da9b9a89..526b943b 100644 --- a/src/database/repository/mod_versions.rs +++ b/src/database/repository/mod_versions.rs @@ -60,6 +60,7 @@ impl ModVersionRow { } } +#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id, version = %version))] pub async fn get_by_version_str( mod_id: &str, version: &str, @@ -83,11 +84,11 @@ pub async fn get_by_version_str( ) .fetch_optional(conn) .await - .inspect_err(|e| tracing::error!("Failed to get mod_version by version string: {e}")) .map_err(|e| e.into()) .map(|opt| opt.map(|x| x.into_mod_version())) } +#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id, statuses = ?statuses))] pub async fn get_for_mod( mod_id: &str, statuses: Option<&[ModVersionStatusEnum]>, @@ -111,8 +112,7 @@ pub async fn get_for_mod( statuses as Option<&[ModVersionStatusEnum]> ) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to get mod_versions for mod {mod_id}: {e}"))?; + .await?; let version_ids: Vec = records.iter().map(|x| x.id).collect(); let mut gd_versions = ModGDVersion::get_for_mod_versions(&version_ids, conn).await?; @@ -129,6 +129,7 @@ pub async fn get_for_mod( Ok(versions) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "UPDATE mod_versions @@ -137,12 +138,12 @@ pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to increment downloads for mod_version {id}: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err, fields(mod_id = %json.id, version = %json.version))] pub async fn create_from_json( json: &ModJson, make_accepted: bool, @@ -150,8 +151,7 @@ pub async fn create_from_json( ) -> Result { sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey DEFERRED") .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to update constraint: {e}"))?; + .await?; let geode = Version::parse(&json.geode).or(Err(DatabaseError::InvalidInput( "mod.json geode version is invalid semver".into(), @@ -191,8 +191,7 @@ pub async fn create_from_json( json.requires_patching ) .fetch_one(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to insert mod_version: {e}"))?; + .await?; let id = row.id; @@ -208,13 +207,11 @@ pub async fn create_from_json( id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to set status: {e}"))?; + .await?; sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey IMMEDIATE") .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to update constraint: {e}"))?; + .await?; Ok(ModVersion { id, @@ -242,6 +239,7 @@ pub async fn create_from_json( }) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %version_id, mod_id = %json.id, version = %json.version))] pub async fn update_pending_version( version_id: i32, json: &ModJson, @@ -304,14 +302,7 @@ pub async fn update_pending_version( version_id ) .fetch_one(&mut *conn) - .await - .inspect_err(|err| { - tracing::error!( - "Failed to update pending version {}-{}: {err}", - json.id, - json.version - ) - })?; + .await?; if make_accepted { sqlx::query!( @@ -321,8 +312,7 @@ pub async fn update_pending_version( row.status_id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to update tag for mod: {e}"))?; + .await?; } Ok(ModVersion { @@ -354,6 +344,7 @@ pub async fn update_pending_version( }) } +#[tracing::instrument(skip_all, err, fields(mod_version_id = %version.id, status = ?status))] pub async fn update_version_status( mut version: ModVersion, status: ModVersionStatusEnum, @@ -378,8 +369,7 @@ pub async fn update_version_status( version.id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to update mod_version_status: {e}"))?; + .await?; version.status = status; diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index 1dd03267..19ef12d1 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -42,6 +42,7 @@ impl ModRecordGetOne { /// Fetches information for a mod, without versions or other added info. /// /// The second parameter decides if about.md and changelog.md are fetched from the database. Those are pretty big files, so only fetch them if needed. +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn get_one( id: &str, include_md: bool, @@ -59,7 +60,6 @@ pub async fn get_one( ) .fetch_optional(conn) .await - .inspect_err(|e| tracing::error!("Failed to fetch mod {id}: {e}")) .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } else { @@ -74,13 +74,13 @@ pub async fn get_one( ) .fetch_optional(conn) .await - .inspect_err(|e| tracing::error!("Failed to fetch mod {}: {}", id, e)) .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } } /// Does NOT check if the target mod exists +#[tracing::instrument(skip_all, err, fields(mod_id = %json.id))] pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result { sqlx::query_as!( ModRecordGetOne, @@ -104,11 +104,11 @@ pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result Result { Ok(sqlx::query!("SELECT featured FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to check if mod {id} is featured: {e}"))? + .await? .map(|row| row.featured) .unwrap_or(false)) } +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn exists(id: &str, conn: &mut PgConnection) -> Result { Ok(sqlx::query!("SELECT id FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to check if mod {} exists: {}", id, e))? + .await? .is_some()) } /// Checks if multiple ids exist in the database. /// /// Returns a tuple with (existing ids, missing ids). +#[tracing::instrument(skip_all, err, fields(mod_ids = ?ids))] pub async fn exists_multiple( ids: &[String], conn: &mut PgConnection, ) -> Result<(Vec, Vec), DatabaseError> { let mods: HashSet = sqlx::query!("SELECT id FROM mods WHERE id = ANY($1)", ids) .fetch_all(&mut *conn) - .await - .inspect_err(|e| tracing::error!("mods::exists_multiple failed: {e}"))? + .await? .into_iter() .map(|x| x.id) .collect(); @@ -211,6 +202,7 @@ pub async fn exists_multiple( Ok((existing, missing)) } +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result>, DatabaseError> { struct QueryResult { image: Option>, @@ -227,8 +219,7 @@ pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result Result Result<(), DatabaseError> { sqlx::query!( "UPDATE mods @@ -247,12 +239,12 @@ pub async fn increment_downloads(id: &str, conn: &mut PgConnection) -> Result<() id ) .execute(&mut *conn) - .await - .inspect_err(|e| tracing::error!("Failed to increment downloads for mod {id}: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err, fields(mod_id = %the_mod.id))] pub async fn update_with_json( mut the_mod: Mod, json: &ModJson, @@ -273,8 +265,7 @@ pub async fn update_with_json( the_mod.id ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to update mod: {}", e))?; + .await?; the_mod.repository = json.repository.clone(); the_mod.about = json.about.clone(); @@ -283,6 +274,7 @@ pub async fn update_with_json( Ok(the_mod) } +#[tracing::instrument(skip_all, err, fields(mod_id = %the_mod.id))] pub async fn update_with_json_moved( mut the_mod: Mod, json: ModJson, @@ -303,8 +295,7 @@ pub async fn update_with_json_moved( the_mod.id ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to update mod: {e}"))?; + .await?; the_mod.repository = json.repository; the_mod.about = json.about; @@ -315,6 +306,7 @@ pub async fn update_with_json_moved( /// Used when first version goes from pending to accepted. /// Makes it so versions that stay a lot in pending appear at the top of the newly created lists +#[tracing::instrument(skip_all, err, fields(mod_id = %id))] pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "UPDATE mods @@ -323,8 +315,7 @@ pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), D id ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to touch created_at for mod {id}: {e}"))?; + .await?; Ok(()) } diff --git a/src/database/repository/refresh_tokens.rs b/src/database/repository/refresh_tokens.rs index d1575042..07d074b3 100644 --- a/src/database/repository/refresh_tokens.rs +++ b/src/database/repository/refresh_tokens.rs @@ -3,6 +3,7 @@ use chrono::{Days, Utc}; use sqlx::PgConnection; use uuid::Uuid; +#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] pub async fn generate_token( developer_id: i32, conn: &mut PgConnection, @@ -19,12 +20,12 @@ pub async fn generate_token( expiry ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to insert refresh token: {e}"))?; + .await?; Ok(token) } +#[tracing::instrument(skip_all, err)] pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let hash = sha256::digest(token.to_string()); sqlx::query!( @@ -33,12 +34,12 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da hash ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to remove refresh token: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] pub async fn remove_developer_tokens( developer_id: i32, conn: &mut PgConnection, @@ -49,20 +50,19 @@ pub async fn remove_developer_tokens( developer_id ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Failed to remove refresh tokens: {e}"))?; + .await?; Ok(()) } +#[tracing::instrument(skip_all, err)] pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM refresh_tokens WHERE expires_at < NOW()" ) .execute(conn) - .await - .inspect_err(|e| tracing::error!("Refresh token cleanup failed: {e}"))?; + .await?; Ok(()) } diff --git a/src/types/models/dependency.rs b/src/types/models/dependency.rs index 25a3ecaf..fabd0437 100644 --- a/src/types/models/dependency.rs +++ b/src/types/models/dependency.rs @@ -108,6 +108,7 @@ pub enum DependencyImportance { } impl Dependency { + #[tracing::instrument(skip_all, err, fields(mod_version_ids = ?ids))] pub async fn get_for_mod_versions( ids: &Vec, platform: Option, @@ -190,8 +191,7 @@ impl Dependency { .bind(geode.map(|x| i32::try_from(x.patch).unwrap_or_default())) .bind(geode_pre) .fetch_all(&mut *pool) - .await - .inspect_err(|x| tracing::error!("Failed to fetch dependencies: {x}"))?; + .await?; let mut ret: HashMap> = HashMap::new(); for i in result { diff --git a/src/types/models/gd_version_alias.rs b/src/types/models/gd_version_alias.rs index de6545dd..d83353ed 100644 --- a/src/types/models/gd_version_alias.rs +++ b/src/types/models/gd_version_alias.rs @@ -20,6 +20,7 @@ pub struct GDVersionAlias { } impl GDVersionAlias { + #[tracing::instrument(skip_all, err, fields(platform = ?platform, identifier = %identifier))] pub async fn find( platform: VerPlatform, identifier: &str, diff --git a/src/types/models/incompatibility.rs b/src/types/models/incompatibility.rs index fbe57e9c..7b6be4e7 100644 --- a/src/types/models/incompatibility.rs +++ b/src/types/models/incompatibility.rs @@ -91,6 +91,7 @@ impl FetchedIncompatibility { } impl Incompatibility { + #[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] pub async fn get_for_mod_version( id: i32, pool: &mut PgConnection, @@ -106,10 +107,10 @@ impl Incompatibility { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| tracing::error!("Failed to fetch incompatibilities for mod_version {id}: {e}")) .map_err(|e| e.into()) } + #[tracing::instrument(skip_all, err, fields(mod_version_ids = ?ids))] pub async fn get_for_mod_versions( ids: &Vec, platform: Option, @@ -161,9 +162,7 @@ impl Incompatibility { .bind(geode.map(|x| i64::try_from(x.patch).ok())) .bind(geode_pre); - let result = q.fetch_all(&mut *pool).await.inspect_err(|e| { - tracing::error!("Failed to fetch incompatibilities for mod_versions: {e}") - })?; + let result = q.fetch_all(&mut *pool).await?; let mut ret: HashMap> = HashMap::new(); diff --git a/src/types/models/loader_version.rs b/src/types/models/loader_version.rs index 7d1d855e..c8940e4b 100644 --- a/src/types/models/loader_version.rs +++ b/src/types/models/loader_version.rs @@ -77,6 +77,7 @@ impl LoaderVersionGetOne { } impl LoaderVersion { + #[tracing::instrument(skip_all, err, fields(gd = ?gd, platform = ?platform, accept_prereleases = %accept_prereleases))] pub async fn get_latest( gd: Option, platform: Option, @@ -168,6 +169,7 @@ impl LoaderVersion { .map(|x| x.map(|y| y.into_loader_version())) } + #[tracing::instrument(skip_all, err, fields(tag = %tag))] pub async fn get_one( tag: &str, pool: &mut PgConnection, @@ -187,6 +189,7 @@ impl LoaderVersion { .map(|x| x.map(|y| y.into_loader_version())) } + #[tracing::instrument(skip_all, err, fields(tag = %version.tag))] pub async fn create_version( version: LoaderVersionCreate, pool: &mut PgConnection, @@ -210,6 +213,7 @@ impl LoaderVersion { .map_err(|e| e.into()) } + #[tracing::instrument(skip_all, err, fields(page = %page, per_page = %per_page))] pub async fn get_many( query: GetVersionsQuery, per_page: i64, diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index 18d036af..be286182 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -116,6 +116,7 @@ impl Mod { } } + #[tracing::instrument(skip_all, err)] pub async fn get_stats(pool: &mut PgConnection) -> Result { let result = sqlx::query!( " @@ -131,8 +132,7 @@ impl Mod { " ) .fetch_optional(&mut *pool) - .await - .inspect_err(|e| tracing::error!("failed to get mod stats: {}", e))?; + .await?; if let Some((Some(total_count), Some(total_downloads))) = result.map(|o| (o.id_count, o.download_sum)) @@ -149,6 +149,7 @@ impl Mod { } } + #[tracing::instrument(skip_all, err, fields(query = ?query.query, page = ?query.page, per_page = ?query.per_page))] pub async fn get_index( pool: &mut PgConnection, query: &IndexQueryParams, @@ -361,8 +362,7 @@ impl Mod { let records: Vec = records_builder .build_query_as() .fetch_all(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch mod index: {}", e))?; + .await?; let mut count_builder = sqlx::QueryBuilder::new("SELECT COUNT(DISTINCT m.id) "); @@ -371,8 +371,7 @@ impl Mod { let count: i64 = count_builder .build_query_scalar() .fetch_optional(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch mod index count: {}", e))? + .await? .unwrap_or_default(); if records.is_empty() { @@ -486,6 +485,7 @@ impl Mod { }) } + #[tracing::instrument(skip_all, err, fields(developer_id = %id, status = ?status, only_owner = %only_owner))] pub async fn get_all_for_dev( id: i32, status: ModVersionStatusEnum, @@ -528,8 +528,7 @@ impl Mod { only_owner ) .fetch_all(&mut *pool) - .await - .inspect_err(|x| tracing::error!("Failed to fetch developer mods: {}", x))?; + .await?; if records.is_empty() { return Ok(vec![]); @@ -572,6 +571,7 @@ impl Mod { Ok(mods) } + #[tracing::instrument(skip_all, err, fields(mod_id = %id, only_accepted = %only_accepted))] pub async fn get_one( id: &str, only_accepted: bool, @@ -596,8 +596,7 @@ impl Mod { only_accepted ) .fetch_all(&mut *pool) - .await - .inspect_err(|e| tracing::error!("{}", e))?; + .await?; if records.is_empty() { return Ok(None); @@ -670,6 +669,7 @@ impl Mod { /// At the moment this is only used to set the mod to featured. /// DOES NOT check if the mod exists + #[tracing::instrument(skip_all, err, fields(mod_id = %id, featured = %featured))] pub async fn update_mod( id: &str, featured: bool, @@ -678,11 +678,11 @@ impl Mod { sqlx::query!("UPDATE mods SET featured = $1 WHERE id = $2", featured, id) .execute(&mut *pool) .await - .inspect_err(|e| tracing::error!("Failed to update mod {id}: {e}")) .map_err(|e| e.into()) .map(|_| ()) } + #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids, platform = ?platforms, gd = ?gd))] pub async fn get_updates( ids: &[String], platforms: VerPlatform, @@ -748,8 +748,7 @@ impl Mod { geode_pre ) .fetch_all(&mut *pool) - .await - .inspect_err(|x| tracing::error!("Failed to fetch mod updates: {}", x))?; + .await?; if result.is_empty() { return Ok(vec![]); diff --git a/src/types/models/mod_gd_version.rs b/src/types/models/mod_gd_version.rs index a201182e..b00c07e6 100644 --- a/src/types/models/mod_gd_version.rs +++ b/src/types/models/mod_gd_version.rs @@ -231,6 +231,7 @@ impl DetailedGDVersion { impl ModGDVersion { // to be used for GET mods/{id}/version/{version} + #[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] pub async fn get_for_mod_version( id: i32, pool: &mut PgConnection, @@ -245,10 +246,7 @@ impl ModGDVersion { id ) .fetch_all(&mut *pool) - .await - .inspect_err(|e| { - tracing::error!("Failed to fetch mod_gd_versions for mod_version {id}: {e}") - })?; + .await?; let mut ret = DetailedGDVersion { win: None, mac: None, @@ -282,6 +280,7 @@ impl ModGDVersion { } // hello + #[tracing::instrument(skip_all, err, fields(mod_version_ids = ?versions))] pub async fn get_for_mod_versions( versions: &[i32], pool: &mut PgConnection, @@ -299,8 +298,7 @@ impl ModGDVersion { versions ) .fetch_all(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch mod_gd_versions: {}", e))?; + .await?; let mut ret: HashMap = HashMap::new(); for i in result { diff --git a/src/types/models/mod_link.rs b/src/types/models/mod_link.rs index fdee90b3..4beab1e5 100644 --- a/src/types/models/mod_link.rs +++ b/src/types/models/mod_link.rs @@ -14,6 +14,7 @@ pub struct ModLinks { } impl ModLinks { + #[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] pub async fn fetch( mod_id: &str, pool: &mut PgConnection, @@ -28,10 +29,10 @@ impl ModLinks { ) .fetch_optional(pool) .await - .inspect_err(|e| tracing::error!("Failed to fetch mod links for mod {}. Error: {}", mod_id, e)) .map_err(|e| e.into()) } + #[tracing::instrument(skip_all, err, fields(mod_ids = ?mod_ids))] pub async fn fetch_for_mods( mod_ids: &Vec, pool: &mut PgConnection, @@ -50,7 +51,6 @@ impl ModLinks { ) .fetch_all(pool) .await - .inspect_err(|e| tracing::error!("Failed to fetch mod links for multiple mods. Error: {}", e)) .map_err(|e| e.into()) } } diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index 586932ff..cc939325 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -149,6 +149,7 @@ impl ModVersion { self.modify_download_link(app_url) } + #[tracing::instrument(skip_all, err, fields(mod_id = %query.mod_id, page = %query.page, per_page = %query.per_page))] pub async fn get_index( query: IndexQuery, pool: &mut PgConnection, @@ -248,14 +249,12 @@ impl ModVersion { let records = q .build_query_as::() .fetch_all(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch index: {e}"))?; + .await?; let count: i64 = counter_q .build_query_scalar() .fetch_one(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch index count: {e}"))?; + .await?; if records.is_empty() { return Ok(PaginatedData { @@ -299,6 +298,7 @@ impl ModVersion { Ok(PaginatedData { data: ret, count }) } + #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids, gd = ?gd))] pub async fn get_latest_for_mods( pool: &mut PgConnection, ids: &[String], @@ -371,7 +371,6 @@ impl ModVersion { .bind(requires_patching) .fetch_all(&mut *pool) .await - .inspect_err(|x| tracing::error!("Failed to fetch latest versions for mods: {}", x)) .map_err(|e| e.into()) .map(|result: Vec| { result.into_iter() @@ -380,6 +379,7 @@ impl ModVersion { }) } + #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids))] pub async fn get_pending_for_mods( ids: &[String], pool: &mut PgConnection, @@ -402,8 +402,7 @@ impl ModVersion { ORDER BY mv.id DESC"#, ids ).fetch_all(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch pending mod versions: {}", e))?; + .await?; let mut ret: HashMap> = HashMap::new(); @@ -416,6 +415,7 @@ impl ModVersion { Ok(ret) } + #[tracing::instrument(skip_all, err, fields(mod_id = %id, gd = ?gd))] pub async fn get_latest_for_mod( id: &str, gd: Option, @@ -471,8 +471,7 @@ impl ModVersion { let version = query_builder .build_query_as::() .fetch_optional(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch latest mod_version for mod {id}: {e}"))? + .await? .map(|v| v.into_mod_version()); let Some(mut version) = version else { @@ -505,6 +504,7 @@ impl ModVersion { Ok(Some(version)) } + #[tracing::instrument(skip_all, err, fields(mod_id = %id, version = %version))] pub async fn get_one( id: &str, version: &str, @@ -531,8 +531,7 @@ impl ModVersion { fetch_only_accepted ) .fetch_optional(&mut *pool) - .await - .inspect_err(|e| tracing::error!("ModVersion::get_one failed: {e}"))? + .await? .map(|x| x.into_mod_version()); let Some(mut version) = result else { @@ -562,6 +561,7 @@ impl ModVersion { Ok(Some(version)) } + #[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] pub async fn get_accepted_count( mod_id: &str, pool: &mut PgConnection, diff --git a/src/types/models/stats.rs b/src/types/models/stats.rs index 55b82156..585d6300 100644 --- a/src/types/models/stats.rs +++ b/src/types/models/stats.rs @@ -29,6 +29,7 @@ pub struct Stats { } impl Stats { + #[tracing::instrument(skip_all, err)] pub async fn get_cached(pool: &mut PgConnection) -> Result { let mod_stats = Mod::get_stats(&mut *pool).await?; Ok(Stats { @@ -40,6 +41,7 @@ impl Stats { }) } + #[tracing::instrument(skip_all, err)] async fn get_latest_github_release_download_count( pool: &mut PgConnection, ) -> Result { @@ -67,8 +69,7 @@ impl Stats { new.1 ) .execute(&mut *pool) - .await - .inspect_err(|e| tracing::error!("{}", e))?; + .await?; Ok(new.0) } diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index 1dccd0cc..ff57505e 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -14,6 +14,7 @@ pub struct Tag { } impl Tag { + #[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] pub async fn get_tags_for_mod( mod_id: &str, pool: &mut PgConnection, @@ -26,11 +27,11 @@ impl Tag { ) .fetch_all(&mut *pool) .await - .inspect_err(|e| tracing::error!("{}", e)) .map(|tags| tags.into_iter().map(|t| t.name).collect::>()) .map_err(|e| e.into()) } + #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids))] pub async fn get_tags_for_mods( ids: &Vec, pool: &mut PgConnection, @@ -42,8 +43,7 @@ impl Tag { ids ) .fetch_all(&mut *pool) - .await - .inspect_err(|e| tracing::error!("{}", e))?; + .await?; let mut ret: HashMap> = HashMap::new(); for tag in tags { @@ -57,6 +57,7 @@ impl Tag { Ok(ret) } + #[tracing::instrument(skip_all, err, fields(tags = %tags))] pub async fn parse_tags(tags: &str, pool: &mut PgConnection) -> Result, ApiError> { let tags = tags .split(',') @@ -68,8 +69,7 @@ impl Tag { &tags ) .fetch_all(&mut *pool) - .await - .inspect_err(|e| tracing::error!("Failed to fetch tags: {}", e))?; + .await?; let fetched_ids = fetched.iter().map(|t| t.id).collect::>(); let fetched_names = fetched From 84a572225de1c3c94308afc40b978da868687813 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 02:40:37 +0300 Subject: [PATCH 47/69] feat: log map_err calls that didn't use original err --- src/endpoints/auth/github.rs | 5 ++++- src/endpoints/mod_status_badge.rs | 5 ++++- src/types/mod_json.rs | 36 ++++++++++++++++++++++++------- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/endpoints/auth/github.rs b/src/endpoints/auth/github.rs index 484e4060..2d01f48d 100644 --- a/src/endpoints/auth/github.rs +++ b/src/endpoints/auth/github.rs @@ -285,7 +285,10 @@ pub async fn github_token_login( Err(_) => client .get_installation(&json.token) .await - .map_err(|_| ApiError::BadRequest(format!("Invalid access token: {}", json.token)))?, + .map_err(|e| { + tracing::error!(error = ?e, "invalid access token"); + ApiError::BadRequest(format!("Invalid access token: {}", json.token)) + })?, Ok(u) => u, }; diff --git a/src/endpoints/mod_status_badge.rs b/src/endpoints/mod_status_badge.rs index 76938efd..37c033ff 100644 --- a/src/endpoints/mod_status_badge.rs +++ b/src/endpoints/mod_status_badge.rs @@ -71,7 +71,10 @@ pub async fn status_badge( .static_storage() .read(svg_path) .await - .map_err(|_| ApiError::InternalError("Failed to read status badge file".into()))?; + .map_err(|e| { + tracing::error!(error = ?e, "failed to read status badge file"); + ApiError::InternalError("Failed to read status badge file".into()) + })?; let api_url = format!("{}/v1/mods/{}?abbreviate=true", data.app_url(), id); let mod_link = format!("{}/mods/{}", data.front_url(), id); let svg_data_url = format!("data:image/svg+xml;utf8,{}", urlencoding::encode_binary(&svg)); diff --git a/src/types/mod_json.rs b/src/types/mod_json.rs index 29a8bbe7..d3638af5 100644 --- a/src/types/mod_json.rs +++ b/src/types/mod_json.rs @@ -306,7 +306,8 @@ impl ModJson { continue; } let (dependency_ver, compare) = split_version_and_compare(&(i.version)) - .map_err(|_| { + .map_err(|e| { + tracing::error!(error = ?e, "invalid semver in dependency version"); ModZipError::InvalidModJson(format!("Invalid semver {}", i.version)) })?; ret.push(DependencyCreate { @@ -329,7 +330,11 @@ impl ModJson { match dep { ModJsonDependencyType::Version(version) => { let (dependency_ver, compare) = split_version_and_compare(version) - .map_err(|_| { + .map_err(|e| { + tracing::error!( + error = ?e, + "invalid semver in dependency version" + ); ModZipError::InvalidModJson(format!( "Invalid semver {}", version @@ -359,7 +364,11 @@ impl ModJson { } }; let (dependency_ver, compare) = split_version_and_compare(dep_ver) - .map_err(|_| { + .map_err(|e| { + tracing::error!( + error = ?e, + "invalid semver in dependency version" + ); ModZipError::InvalidModJson(format!( "Invalid semver {}", dep_ver @@ -406,7 +415,8 @@ impl ModJson { continue; } - let (ver, compare) = split_version_and_compare(&(i.version)).map_err(|_| { + let (ver, compare) = split_version_and_compare(&(i.version)).map_err(|e| { + tracing::error!(error = ?e, "invalid semver in incompatibility version"); ModZipError::InvalidModJson(format!("Invalid semver: {}", i.version)) })?; ret.push(IncompatibilityCreate { @@ -430,7 +440,11 @@ impl ModJson { match item { ModJsonIncompatibilityType::Version(version) => { let (ver, compare) = - split_version_and_compare(version).map_err(|_| { + split_version_and_compare(version).map_err(|e| { + tracing::error!( + error = ?e, + "invalid semver in incompatibility version" + ); ModZipError::InvalidModJson(format!( "Invalid semver {}", version @@ -460,7 +474,11 @@ impl ModJson { } }; let (ver, compare) = - split_version_and_compare(inc_ver).map_err(|_| { + split_version_and_compare(inc_ver).map_err(|e| { + tracing::error!( + error = ?e, + "invalid semver in incompatibility version" + ); ModZipError::InvalidModJson(format!( "Invalid semver {}", inc_ver @@ -532,8 +550,10 @@ impl ModJson { } let v5: bool = { - let geode = Version::parse(self.geode.trim_start_matches('v')) - .map_err(|_| ModZipError::InvalidModJson("Invalid geode version".into()))?; + let geode = Version::parse(self.geode.trim_start_matches('v')).map_err(|e| { + tracing::error!(error = ?e, "invalid semver in geode version"); + ModZipError::InvalidModJson("Invalid geode version".into()) + })?; geode.major >= 5 }; From 4c48856a54c1ecd16bcfab41d3a0ac7d893405dd Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 02:54:21 +0300 Subject: [PATCH 48/69] chore: adapt deployment to tracing --- .env.example | 6 ++++++ Dockerfile | 2 -- config/log4rs.example.yaml | 32 -------------------------------- docs/dev_setup.md | 8 +++----- 4 files changed, 9 insertions(+), 39 deletions(-) delete mode 100644 config/log4rs.example.yaml diff --git a/.env.example b/.env.example index a70b276b..0310196a 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,12 @@ FRONT_URL=http://localhost:5173 DATABASE_URL=postgres://user:password@localhost/schema PORT=8080 +# Logging (see tracing/tracing-subscriber docs) +# RUST_LOG controls verbosity, e.g. "info", "debug", "info,sqlx=warn", "geode_index=debug" +RUST_LOG=info +# LOG_FORMAT: "text" (default, human-readable) or "json" (structured, for log aggregation) +LOG_FORMAT=text + # GitHub GITHUB_CLIENT_ID= diff --git a/Dockerfile b/Dockerfile index 62769d24..3410d0d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -88,7 +88,6 @@ RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations -COPY config ./config RUN addgroup -S -g 1000 geode && adduser -S -u 1000 geode -G geode \ && mkdir -p storage \ @@ -110,7 +109,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations -COPY config ./config RUN groupadd --system --gid 1000 geode && useradd --system --uid 1000 --gid geode geode \ && mkdir -p storage \ diff --git a/config/log4rs.example.yaml b/config/log4rs.example.yaml deleted file mode 100644 index 4cdfff1b..00000000 --- a/config/log4rs.example.yaml +++ /dev/null @@ -1,32 +0,0 @@ -appenders: - console: - kind: console - encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S)} {M}:{L} {h({l})} {m}{n}" - daily: - kind: rolling_file - path: "log/index.log" - encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S)} {M}:{L} {h({l})} {m}{n}" - policy: - trigger: - kind: time - interval: 1 day - roller: - kind: fixed_window - pattern: "log/index.{}.log" - base: 0 - count: 10 -root: - level: info - -loggers: - geode_index: - level: info - appenders: - - console - - daily - actix_server: - level: info - appenders: - - console \ No newline at end of file diff --git a/docs/dev_setup.md b/docs/dev_setup.md index 9a50fb18..829b1a23 100644 --- a/docs/dev_setup.md +++ b/docs/dev_setup.md @@ -69,11 +69,9 @@ Third, we need to setup a local GitHub OAuth app. Since the index doesn't store ## 4. Logging -Next up, set up the log4rs config file found in `config`: -```bash -cp config/log4rs.example.yaml config/log4rs.yaml -``` -Feel free to change the settings, but the default works fine. +Logging is configured via environment variables. See the `.env.example` file for the available options: +- `RUST_LOG`: Controls log verbosity (e.g., "info", "debug", "info,sqlx=warn", "geode_index=debug"). Defaults to "info,sqlx=warn,tracing_actix_web=info" when unset. +- `LOG_FORMAT`: Output format - "text" (default, human-readable) or "json" (structured, for log aggregation). After all of this is done, you should be able to run `cargo run` inside the index directory. The migrations will be ran automatically, and the index will start. You can check `http://localhost:8080` (if you haven't changed the port in your .env file) to see if it all works. From 19c8e76effdf0dc4965a75f7b1126b3454120383 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 02:56:22 +0300 Subject: [PATCH 49/69] fix: remove instrument err from handlers + db --- src/database/repository/auth_tokens.rs | 8 ++-- src/database/repository/dependencies.rs | 4 +- src/database/repository/deprecations.rs | 14 +++---- src/database/repository/developers.rs | 36 ++++++++-------- .../repository/github_login_attempts.rs | 10 ++--- src/database/repository/github_web_logins.rs | 6 +-- src/database/repository/incompatibilities.rs | 4 +- src/database/repository/mod_downloads.rs | 6 +-- src/database/repository/mod_gd_versions.rs | 4 +- src/database/repository/mod_links.rs | 2 +- src/database/repository/mod_tags.rs | 10 ++--- .../repository/mod_version_statuses.rs | 2 +- .../repository/mod_version_submissions.rs | 42 +++++++++---------- src/database/repository/mod_versions.rs | 12 +++--- src/database/repository/mods.rs | 26 ++++++------ src/database/repository/refresh_tokens.rs | 8 ++-- src/endpoints/auth/github.rs | 10 ++--- src/endpoints/auth/mod.rs | 2 +- src/endpoints/deprecations.rs | 10 ++--- src/endpoints/developers.rs | 20 ++++----- src/endpoints/loader.rs | 6 +-- src/endpoints/mod_status_badge.rs | 2 +- src/endpoints/mod_version_submissions.rs | 22 +++++----- src/endpoints/mod_versions.rs | 10 ++--- src/endpoints/mods.rs | 12 +++--- src/endpoints/stats.rs | 2 +- src/endpoints/tags.rs | 4 +- src/types/models/dependency.rs | 2 +- src/types/models/gd_version_alias.rs | 2 +- src/types/models/incompatibility.rs | 4 +- src/types/models/loader_version.rs | 8 ++-- src/types/models/mod_entity.rs | 12 +++--- src/types/models/mod_gd_version.rs | 4 +- src/types/models/mod_link.rs | 4 +- src/types/models/mod_version.rs | 12 +++--- src/types/models/stats.rs | 4 +- src/types/models/tag.rs | 6 +-- 37 files changed, 176 insertions(+), 176 deletions(-) diff --git a/src/database/repository/auth_tokens.rs b/src/database/repository/auth_tokens.rs index 2d8c2fd9..345d80ce 100644 --- a/src/database/repository/auth_tokens.rs +++ b/src/database/repository/auth_tokens.rs @@ -4,7 +4,7 @@ use sqlx::PgConnection; use uuid::Uuid; /// Assumes developer ID exists -#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] +#[tracing::instrument(skip_all, fields(developer_id = %developer_id))] pub async fn generate_token( developer_id: i32, with_expiry: bool, @@ -33,7 +33,7 @@ pub async fn generate_token( Ok(token) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let hash = sha256::digest(token.to_string()); @@ -48,7 +48,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da Ok(()) } -#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] +#[tracing::instrument(skip_all, fields(developer_id = %developer_id))] pub async fn remove_developer_tokens( developer_id: i32, conn: &mut PgConnection, @@ -64,7 +64,7 @@ pub async fn remove_developer_tokens( Ok(()) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM auth_tokens diff --git a/src/database/repository/dependencies.rs b/src/database/repository/dependencies.rs index ce1c6104..957a55d3 100644 --- a/src/database/repository/dependencies.rs +++ b/src/database/repository/dependencies.rs @@ -8,7 +8,7 @@ use crate::{ }, }; -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, json: &ModJson, @@ -63,7 +63,7 @@ pub async fn create( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %id))] pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM dependencies diff --git a/src/database/repository/deprecations.rs b/src/database/repository/deprecations.rs index 273f2e6c..a56055b4 100644 --- a/src/database/repository/deprecations.rs +++ b/src/database/repository/deprecations.rs @@ -3,7 +3,7 @@ use crate::types::models::deprecations::Deprecation; use crate::types::models::developer::Developer; use sqlx::PgConnection; -#[tracing::instrument(skip_all, err, fields(mod_ids = ?ids))] +#[tracing::instrument(skip_all, fields(mod_ids = ?ids))] pub async fn get_for_mods( ids: &[String], conn: &mut PgConnection, @@ -40,7 +40,7 @@ pub async fn get_for_mods( .collect()) } -#[tracing::instrument(skip_all, err, fields(deprecation_id = %id))] +#[tracing::instrument(skip_all, fields(deprecation_id = %id))] pub async fn get(id: i32, conn: &mut PgConnection) -> Result, DatabaseError> { let dep = sqlx::query!( "SELECT @@ -80,7 +80,7 @@ pub async fn get(id: i32, conn: &mut PgConnection) -> Result Ok(Some(dep)) } -#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] +#[tracing::instrument(skip_all, fields(mod_id = %mod_id))] pub async fn create( mod_id: &str, by: &[String], @@ -112,7 +112,7 @@ pub async fn create( }) } -#[tracing::instrument(skip_all, err, fields(deprecation_id = %deprecation.id))] +#[tracing::instrument(skip_all, fields(deprecation_id = %deprecation.id))] pub async fn update( mut deprecation: Deprecation, by: Option<&[String]>, @@ -175,7 +175,7 @@ pub async fn update( Ok(deprecation) } -#[tracing::instrument(skip_all, err, fields(deprecation_id = %id))] +#[tracing::instrument(skip_all, fields(deprecation_id = %id))] pub async fn delete(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM deprecations @@ -188,7 +188,7 @@ pub async fn delete(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseErro Ok(()) } -#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] +#[tracing::instrument(skip_all, fields(mod_id = %mod_id))] pub async fn clear_all(mod_id: &str, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM deprecations @@ -201,7 +201,7 @@ pub async fn clear_all(mod_id: &str, conn: &mut PgConnection) -> Result<(), Data Ok(()) } -#[tracing::instrument(skip_all, err, fields(deprecation_id = %id))] +#[tracing::instrument(skip_all, fields(deprecation_id = %id))] async fn insert_deprecated_by( id: i32, by: &[String], diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index fef7fa59..38c4829f 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -5,7 +5,7 @@ use sqlx::PgConnection; use std::collections::HashMap; use uuid::Uuid; -#[tracing::instrument(skip_all, err, fields(query = ?query))] +#[tracing::instrument(skip_all, fields(query = ?query))] pub async fn index( query: Option<&str>, page: i64, @@ -47,7 +47,7 @@ pub async fn index( }) } -#[tracing::instrument(skip_all, err, fields(query = ?query))] +#[tracing::instrument(skip_all, fields(query = ?query))] pub async fn index_count( query: Option<&str>, conn: &mut PgConnection, @@ -67,7 +67,7 @@ pub async fn index_count( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(github_id = %github_id, username = %username))] +#[tracing::instrument(skip_all, fields(github_id = %github_id, username = %username))] pub async fn fetch_or_insert_github( github_id: i64, username: &str, @@ -94,7 +94,7 @@ pub async fn fetch_or_insert_github( } } -#[tracing::instrument(skip_all, err, fields(github_id = %github_id, username = %username))] +#[tracing::instrument(skip_all, fields(github_id = %github_id, username = %username))] async fn insert_github( github_id: i64, username: &str, @@ -119,7 +119,7 @@ async fn insert_github( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(developer_id = %id))] +#[tracing::instrument(skip_all, fields(developer_id = %id))] pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result, DatabaseError> { sqlx::query_as!( Developer, @@ -139,7 +139,7 @@ pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result Result { sqlx::query!( "SELECT mods.id FROM mods @@ -304,7 +304,7 @@ pub async fn has_active_mod(dev_id: i32, conn: &mut PgConnection) -> Result Result { sqlx::query!( "SELECT mod_versions.id diff --git a/src/database/repository/github_login_attempts.rs b/src/database/repository/github_login_attempts.rs index 680a49a6..fb9cadb9 100644 --- a/src/database/repository/github_login_attempts.rs +++ b/src/database/repository/github_login_attempts.rs @@ -5,7 +5,7 @@ use sqlx::types::ipnetwork::IpNetwork; use sqlx::PgConnection; use uuid::Uuid; -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_one_by_ip( ip: IpNetwork, conn: &mut PgConnection, @@ -31,7 +31,7 @@ pub async fn get_one_by_ip( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_one_by_uuid( uuid: Uuid, pool: &mut PgConnection, @@ -57,7 +57,7 @@ pub async fn get_one_by_uuid( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn create( ip: IpNetwork, device_code: String, @@ -94,7 +94,7 @@ pub async fn create( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn poll_now(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let now = Utc::now(); sqlx::query!( @@ -110,7 +110,7 @@ pub async fn poll_now(uuid: Uuid, conn: &mut PgConnection) -> Result<(), Databas Ok(()) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_login_attempts WHERE uid = $1", uuid) .execute(conn) diff --git a/src/database/repository/github_web_logins.rs b/src/database/repository/github_web_logins.rs index fc341ec5..7ddfd8e0 100644 --- a/src/database/repository/github_web_logins.rs +++ b/src/database/repository/github_web_logins.rs @@ -2,7 +2,7 @@ use crate::database::DatabaseError; use sqlx::PgConnection; use uuid::Uuid; -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn create_unique(conn: &mut PgConnection) -> Result { let unique = Uuid::new_v4(); @@ -13,7 +13,7 @@ pub async fn create_unique(conn: &mut PgConnection) -> Result Result { sqlx::query!("SELECT state FROM github_web_logins WHERE state = $1", uuid) .fetch_optional(conn) @@ -22,7 +22,7 @@ pub async fn exists(uuid: Uuid, conn: &mut PgConnection) -> Result Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_web_logins WHERE state = $1", uuid) .execute(conn) diff --git a/src/database/repository/incompatibilities.rs b/src/database/repository/incompatibilities.rs index c4de1ee8..000a9126 100644 --- a/src/database/repository/incompatibilities.rs +++ b/src/database/repository/incompatibilities.rs @@ -11,7 +11,7 @@ use crate::{ }, }; -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, json: &ModJson, @@ -68,7 +68,7 @@ pub async fn create( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %id))] pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM incompatibilities diff --git a/src/database/repository/mod_downloads.rs b/src/database/repository/mod_downloads.rs index a09d2678..467b1435 100644 --- a/src/database/repository/mod_downloads.rs +++ b/src/database/repository/mod_downloads.rs @@ -3,7 +3,7 @@ use chrono::{Days, Utc}; use sqlx::types::ipnetwork::IpNetwork; use sqlx::PgConnection; -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id))] pub async fn create( ip: IpNetwork, mod_version_id: i32, @@ -22,7 +22,7 @@ pub async fn create( Ok(result.rows_affected() > 0) } -#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] +#[tracing::instrument(skip_all, fields(mod_id = %mod_id))] pub async fn has_downloaded_mod( ip: IpNetwork, mod_id: &str, @@ -43,7 +43,7 @@ pub async fn has_downloaded_mod( .map(|x| x.is_some()) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { let date = Utc::now().checked_sub_days(Days::new(30)).unwrap(); sqlx::query!( diff --git a/src/database/repository/mod_gd_versions.rs b/src/database/repository/mod_gd_versions.rs index a8100358..2899bdcb 100644 --- a/src/database/repository/mod_gd_versions.rs +++ b/src/database/repository/mod_gd_versions.rs @@ -8,7 +8,7 @@ use crate::{ }, }; -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, json: &ModJson, @@ -38,7 +38,7 @@ pub async fn create( Ok(json.gd.clone()) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id))] pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM mod_gd_versions mgv diff --git a/src/database/repository/mod_links.rs b/src/database/repository/mod_links.rs index 42a88437..a5dc8c3e 100644 --- a/src/database/repository/mod_links.rs +++ b/src/database/repository/mod_links.rs @@ -2,7 +2,7 @@ use sqlx::PgConnection; use crate::{database::DatabaseError, types::models::mod_link::ModLinks}; -#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] +#[tracing::instrument(skip_all, fields(mod_id = %mod_id))] pub async fn upsert( mod_id: &str, community: Option, diff --git a/src/database/repository/mod_tags.rs b/src/database/repository/mod_tags.rs index 5a095e81..f3d9ce48 100644 --- a/src/database/repository/mod_tags.rs +++ b/src/database/repository/mod_tags.rs @@ -3,7 +3,7 @@ use crate::database::DatabaseError; use crate::types::models::tag::Tag; use sqlx::PgConnection; -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_all_writable(conn: &mut PgConnection) -> Result, DatabaseError> { let tags = sqlx::query!( "SELECT @@ -28,7 +28,7 @@ pub async fn get_all_writable(conn: &mut PgConnection) -> Result, Datab Ok(tags) } -#[tracing::instrument(skip_all, err, fields(mod_id = %id))] +#[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn get_allowed_for_mod( id: &str, conn: &mut PgConnection, @@ -62,7 +62,7 @@ pub async fn get_allowed_for_mod( return Ok(writable); } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> { let tags = sqlx::query!( "SELECT @@ -86,7 +86,7 @@ pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> Ok(tags) } -#[tracing::instrument(skip_all, err, fields(mod_id = %id))] +#[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, DatabaseError> { sqlx::query!( "SELECT @@ -114,7 +114,7 @@ pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, }) } -#[tracing::instrument(skip_all, err, fields(mod_id = %id))] +#[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn update_for_mod( id: &str, tags: &[Tag], diff --git a/src/database/repository/mod_version_statuses.rs b/src/database/repository/mod_version_statuses.rs index c536a032..53eb4da1 100644 --- a/src/database/repository/mod_version_statuses.rs +++ b/src/database/repository/mod_version_statuses.rs @@ -2,7 +2,7 @@ use crate::database::DatabaseError; use crate::types::models::mod_version_status::ModVersionStatusEnum; use sqlx::PgConnection; -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id, status = ?status))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id, status = ?status))] pub async fn create( mod_version_id: i32, status: ModVersionStatusEnum, diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index fa11c819..fda47ecb 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -7,7 +7,7 @@ use crate::types::models::mod_version_submission::{ use sqlx::{Error, PgConnection}; use std::collections::HashMap; -#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %id))] pub async fn get_for_mod_version( id: i32, conn: &mut PgConnection, @@ -26,7 +26,7 @@ pub async fn get_for_mod_version( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(submission_id = %id))] +#[tracing::instrument(skip_all, fields(submission_id = %id))] pub async fn get_audit_for_submission( id: i32, conn: &mut PgConnection, @@ -44,7 +44,7 @@ pub async fn get_audit_for_submission( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id))] pub async fn create( mod_version_id: i32, conn: &mut PgConnection, @@ -63,7 +63,7 @@ pub async fn create( Ok(row) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %mod_version_id, lock = ?lock))] +#[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id, lock = ?lock))] pub async fn set_locked( mod_version_id: i32, lock: ModVersionSubmissionLock, @@ -106,7 +106,7 @@ pub async fn set_locked( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(submission_id = %id))] +#[tracing::instrument(skip_all, fields(submission_id = %id))] pub async fn get_paginated_comments_for_submission( id: i32, page: i64, @@ -131,7 +131,7 @@ pub async fn get_paginated_comments_for_submission( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(submission_id = %id))] +#[tracing::instrument(skip_all, fields(submission_id = %id))] pub async fn count_comments_for_submission( id: i32, conn: &mut PgConnection, @@ -146,7 +146,7 @@ pub async fn count_comments_for_submission( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(submission_id = %submission_id, author_id = %author_id))] +#[tracing::instrument(skip_all, fields(submission_id = %submission_id, author_id = %author_id))] pub async fn create_comment( submission_id: i32, author_id: i32, @@ -167,7 +167,7 @@ pub async fn create_comment( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] +#[tracing::instrument(skip_all, fields(comment_id = %comment_id))] pub async fn get_comment( comment_id: i64, conn: &mut PgConnection, @@ -184,7 +184,7 @@ pub async fn get_comment( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] +#[tracing::instrument(skip_all, fields(comment_id = %comment_id))] pub async fn update_comment( comment_id: i64, new_text: &str, @@ -204,7 +204,7 @@ pub async fn update_comment( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] +#[tracing::instrument(skip_all, fields(comment_id = %comment_id))] pub async fn delete_comment( comment_id: i64, conn: &mut PgConnection, @@ -218,7 +218,7 @@ pub async fn delete_comment( Ok(result.rows_affected() > 0) } -#[tracing::instrument(skip_all, err, fields(comment_id = %id))] +#[tracing::instrument(skip_all, fields(comment_id = %id))] pub async fn get_audit_for_comment( id: i64, conn: &mut PgConnection, @@ -236,7 +236,7 @@ pub async fn get_audit_for_comment( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] +#[tracing::instrument(skip_all, fields(comment_id = %comment_id))] pub async fn count_attachments_for_comment( comment_id: i64, conn: &mut PgConnection, @@ -251,7 +251,7 @@ pub async fn count_attachments_for_comment( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id, filename = %filename))] +#[tracing::instrument(skip_all, fields(comment_id = %comment_id, filename = %filename))] pub async fn create_attachment( comment_id: i64, filename: &str, @@ -270,7 +270,7 @@ pub async fn create_attachment( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_id = %comment_id))] +#[tracing::instrument(skip_all, fields(comment_id = %comment_id))] pub async fn get_attachments_for_comment( comment_id: i64, conn: &mut PgConnection, @@ -288,7 +288,7 @@ pub async fn get_attachments_for_comment( .map_err(|e: Error| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_ids = ?comment_ids))] +#[tracing::instrument(skip_all, fields(comment_ids = ?comment_ids))] pub async fn get_attachments_for_comments( comment_ids: &[i64], conn: &mut PgConnection, @@ -313,7 +313,7 @@ pub async fn get_attachments_for_comments( Ok(ret) } -#[tracing::instrument(skip_all, err, fields(attachment_id = %attachment_id))] +#[tracing::instrument(skip_all, fields(attachment_id = %attachment_id))] pub async fn get_attachment( attachment_id: i64, conn: &mut PgConnection, @@ -330,7 +330,7 @@ pub async fn get_attachment( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(attachment_id = %attachment_id))] +#[tracing::instrument(skip_all, fields(attachment_id = %attachment_id))] pub async fn delete_attachment( attachment_id: i64, conn: &mut PgConnection, @@ -344,7 +344,7 @@ pub async fn delete_attachment( Ok(result.rows_affected() > 0) } -#[tracing::instrument(skip_all, err, fields(filename = %filename))] +#[tracing::instrument(skip_all, fields(filename = %filename))] pub async fn count_references_to_filename( filename: &str, conn: &mut PgConnection, @@ -359,7 +359,7 @@ pub async fn count_references_to_filename( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(filenames = ?filenames))] +#[tracing::instrument(skip_all, fields(filenames = ?filenames))] pub async fn count_references_to_filenames( filenames: &[String], conn: &mut PgConnection, @@ -382,7 +382,7 @@ pub async fn count_references_to_filenames( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(submission_id = %id, action = ?action))] +#[tracing::instrument(skip_all, fields(submission_id = %id, action = ?action))] pub async fn insert_submission_audit( id: i32, action: AuditAction, @@ -404,7 +404,7 @@ pub async fn insert_submission_audit( .map_err(|e| e.into()) } -#[tracing::instrument(skip_all, err, fields(comment_id = %id, action = ?action))] +#[tracing::instrument(skip_all, fields(comment_id = %id, action = ?action))] pub async fn insert_comment_audit( id: i64, action: AuditAction, diff --git a/src/database/repository/mod_versions.rs b/src/database/repository/mod_versions.rs index 526b943b..045e16db 100644 --- a/src/database/repository/mod_versions.rs +++ b/src/database/repository/mod_versions.rs @@ -60,7 +60,7 @@ impl ModVersionRow { } } -#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id, version = %version))] +#[tracing::instrument(skip_all, fields(mod_id = %mod_id, version = %version))] pub async fn get_by_version_str( mod_id: &str, version: &str, @@ -88,7 +88,7 @@ pub async fn get_by_version_str( .map(|opt| opt.map(|x| x.into_mod_version())) } -#[tracing::instrument(skip_all, err, fields(mod_id = %mod_id, statuses = ?statuses))] +#[tracing::instrument(skip_all, fields(mod_id = %mod_id, statuses = ?statuses))] pub async fn get_for_mod( mod_id: &str, statuses: Option<&[ModVersionStatusEnum]>, @@ -129,7 +129,7 @@ pub async fn get_for_mod( Ok(versions) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] +#[tracing::instrument(skip_all, fields(mod_version_id = %id))] pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "UPDATE mod_versions @@ -143,7 +143,7 @@ pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), Ok(()) } -#[tracing::instrument(skip_all, err, fields(mod_id = %json.id, version = %json.version))] +#[tracing::instrument(skip_all, fields(mod_id = %json.id, version = %json.version))] pub async fn create_from_json( json: &ModJson, make_accepted: bool, @@ -239,7 +239,7 @@ pub async fn create_from_json( }) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %version_id, mod_id = %json.id, version = %json.version))] +#[tracing::instrument(skip_all, fields(mod_version_id = %version_id, mod_id = %json.id, version = %json.version))] pub async fn update_pending_version( version_id: i32, json: &ModJson, @@ -344,7 +344,7 @@ pub async fn update_pending_version( }) } -#[tracing::instrument(skip_all, err, fields(mod_version_id = %version.id, status = ?status))] +#[tracing::instrument(skip_all, fields(mod_version_id = %version.id, status = ?status))] pub async fn update_version_status( mut version: ModVersion, status: ModVersionStatusEnum, diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index 19ef12d1..305dc0d1 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -42,7 +42,7 @@ impl ModRecordGetOne { /// Fetches information for a mod, without versions or other added info. /// /// The second parameter decides if about.md and changelog.md are fetched from the database. Those are pretty big files, so only fetch them if needed. -#[tracing::instrument(skip_all, err, fields(mod_id = %id))] +#[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn get_one( id: &str, include_md: bool, @@ -80,7 +80,7 @@ pub async fn get_one( } /// Does NOT check if the target mod exists -#[tracing::instrument(skip_all, err, fields(mod_id = %json.id))] +#[tracing::instrument(skip_all, fields(mod_id = %json.id))] pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result { sqlx::query_as!( ModRecordGetOne, @@ -108,7 +108,7 @@ pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result Result { Ok(sqlx::query!("SELECT featured FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) @@ -165,7 +165,7 @@ pub async fn is_featured(id: &str, conn: &mut PgConnection) -> Result Result { Ok(sqlx::query!("SELECT id FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) @@ -176,7 +176,7 @@ pub async fn exists(id: &str, conn: &mut PgConnection) -> Result Result>, DatabaseError> { struct QueryResult { image: Option>, @@ -230,7 +230,7 @@ pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result Result<(), DatabaseError> { sqlx::query!( "UPDATE mods @@ -244,7 +244,7 @@ pub async fn increment_downloads(id: &str, conn: &mut PgConnection) -> Result<() Ok(()) } -#[tracing::instrument(skip_all, err, fields(mod_id = %the_mod.id))] +#[tracing::instrument(skip_all, fields(mod_id = %the_mod.id))] pub async fn update_with_json( mut the_mod: Mod, json: &ModJson, @@ -274,7 +274,7 @@ pub async fn update_with_json( Ok(the_mod) } -#[tracing::instrument(skip_all, err, fields(mod_id = %the_mod.id))] +#[tracing::instrument(skip_all, fields(mod_id = %the_mod.id))] pub async fn update_with_json_moved( mut the_mod: Mod, json: ModJson, @@ -306,7 +306,7 @@ pub async fn update_with_json_moved( /// Used when first version goes from pending to accepted. /// Makes it so versions that stay a lot in pending appear at the top of the newly created lists -#[tracing::instrument(skip_all, err, fields(mod_id = %id))] +#[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "UPDATE mods diff --git a/src/database/repository/refresh_tokens.rs b/src/database/repository/refresh_tokens.rs index 07d074b3..75e0d16f 100644 --- a/src/database/repository/refresh_tokens.rs +++ b/src/database/repository/refresh_tokens.rs @@ -3,7 +3,7 @@ use chrono::{Days, Utc}; use sqlx::PgConnection; use uuid::Uuid; -#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] +#[tracing::instrument(skip_all, fields(developer_id = %developer_id))] pub async fn generate_token( developer_id: i32, conn: &mut PgConnection, @@ -25,7 +25,7 @@ pub async fn generate_token( Ok(token) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let hash = sha256::digest(token.to_string()); sqlx::query!( @@ -39,7 +39,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da Ok(()) } -#[tracing::instrument(skip_all, err, fields(developer_id = %developer_id))] +#[tracing::instrument(skip_all, fields(developer_id = %developer_id))] pub async fn remove_developer_tokens( developer_id: i32, conn: &mut PgConnection, @@ -55,7 +55,7 @@ pub async fn remove_developer_tokens( Ok(()) } -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM refresh_tokens diff --git a/src/endpoints/auth/github.rs b/src/endpoints/auth/github.rs index 2d01f48d..14177a79 100644 --- a/src/endpoints/auth/github.rs +++ b/src/endpoints/auth/github.rs @@ -40,7 +40,7 @@ struct CallbackParams { ) )] #[post("v1/login/github")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn start_github_login( data: web::Data, info: ConnectionInfo, @@ -78,7 +78,7 @@ pub async fn start_github_login( ) )] #[post("v1/login/github/web")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn start_github_web_login(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; @@ -108,7 +108,7 @@ pub async fn start_github_web_login(data: web::Data) -> Result, data: web::Data, @@ -166,7 +166,7 @@ pub async fn github_web_callback( ) )] #[post("v1/login/github/poll")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn poll_github_login( json: web::Json, data: web::Data, @@ -271,7 +271,7 @@ pub async fn poll_github_login( ) )] #[post("v1/login/github/token")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn github_token_login( json: web::Json, data: web::Data, diff --git a/src/endpoints/auth/mod.rs b/src/endpoints/auth/mod.rs index ebfd5f35..c089a796 100644 --- a/src/endpoints/auth/mod.rs +++ b/src/endpoints/auth/mod.rs @@ -34,7 +34,7 @@ struct RefreshBody { ) )] #[post("v1/login/refresh")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn refresh_token( json: web::Json, data: web::Data, diff --git a/src/endpoints/deprecations.rs b/src/endpoints/deprecations.rs index af0c3da9..826e38be 100644 --- a/src/endpoints/deprecations.rs +++ b/src/endpoints/deprecations.rs @@ -48,7 +48,7 @@ const MAX_MODS_PER_DEPRECATION: usize = 20; ) )] #[get("v1/mods/{id}/deprecations")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn index( data: web::Data, path: web::Path @@ -85,7 +85,7 @@ pub async fn index( ) )] #[post("v1/mods/{id}/deprecations")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn store( data: web::Data, path: web::Path, @@ -133,7 +133,7 @@ pub async fn store( ) )] #[put("v1/mods/{id}/deprecations/{deprecation_id}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] pub async fn update( data: web::Data, path: web::Path, @@ -204,7 +204,7 @@ pub async fn update( ) )] #[delete("v1/mods/{id}/deprecations/{deprecation_id}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] pub async fn delete( data: web::Data, path: web::Path, @@ -258,7 +258,7 @@ pub async fn delete( ) )] #[delete("v1/mods/{id}/deprecations")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn clear_all( data: web::Data, path: web::Path, diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index d97bc4f9..4acdb0c9 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -82,7 +82,7 @@ struct DeveloperIndexQuery { ) )] #[get("v1/developers")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn developer_index( data: web::Data, query: web::Query, @@ -119,7 +119,7 @@ pub async fn developer_index( ) )] #[post("v1/mods/{id}/developers")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, username = %json.username))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, username = %json.username))] pub async fn add_developer_to_mod( data: web::Data, path: web::Path, @@ -166,7 +166,7 @@ pub async fn add_developer_to_mod( ) )] #[delete("v1/mods/{id}/developers/{username}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, username = %path.username))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, username = %path.username))] pub async fn remove_dev_from_mod( data: web::Data, path: web::Path, @@ -223,7 +223,7 @@ pub async fn remove_dev_from_mod( ) )] #[delete("v1/me/token")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn delete_token( data: web::Data, auth: Auth, @@ -250,7 +250,7 @@ pub async fn delete_token( ) )] #[delete("v1/me/tokens")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn delete_tokens( data: web::Data, auth: Auth, @@ -285,7 +285,7 @@ struct UploadProfilePayload { ) )] #[put("v1/me")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn update_profile( data: web::Data, json: web::Json, @@ -343,7 +343,7 @@ pub fn default_own_mods_status() -> ModVersionStatusEnum { ) )] #[get("v1/me/mods")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_own_mods( data: web::Data, query: web::Query, @@ -373,7 +373,7 @@ pub async fn get_own_mods( ) )] #[get("v1/me")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_me(auth: Auth, data: web::Data) -> Result { let mut pool = data.db().acquire().await?; let dev = auth.developer()?; @@ -403,7 +403,7 @@ struct GetDeveloperPath { ) )] #[get("v1/developers/{id}")] -#[tracing::instrument(skip_all, err, fields(developer_id = %path.id))] +#[tracing::instrument(skip_all, fields(developer_id = %path.id))] pub async fn get_developer( data: web::Data, path: web::Path, @@ -438,7 +438,7 @@ pub async fn get_developer( ) )] #[put("v1/developers/{id}")] -#[tracing::instrument(skip_all, err, fields(developer_id = %path.id))] +#[tracing::instrument(skip_all, fields(developer_id = %path.id))] pub async fn update_developer( auth: Auth, data: web::Data, diff --git a/src/endpoints/loader.rs b/src/endpoints/loader.rs index ce8df27b..eb5df2cc 100644 --- a/src/endpoints/loader.rs +++ b/src/endpoints/loader.rs @@ -44,7 +44,7 @@ struct GetOnePath { ) )] #[get("v1/loader/versions/{version}")] -#[tracing::instrument(skip_all, err, fields(version = %path.version))] +#[tracing::instrument(skip_all, fields(version = %path.version))] pub async fn get_one( path: web::Path, data: web::Data, @@ -109,7 +109,7 @@ struct CreateVersionBody { ) )] #[post("v1/loader/versions")] -#[tracing::instrument(skip_all, err, fields(tag = %payload.tag))] +#[tracing::instrument(skip_all, fields(tag = %payload.tag))] pub async fn create_version( data: web::Data, payload: web::Json, @@ -162,7 +162,7 @@ struct GetManyQuery { ) )] #[get("v1/loader/versions")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_many( data: web::Data, query: web::Query, diff --git a/src/endpoints/mod_status_badge.rs b/src/endpoints/mod_status_badge.rs index 37c033ff..75e8c478 100644 --- a/src/endpoints/mod_status_badge.rs +++ b/src/endpoints/mod_status_badge.rs @@ -39,7 +39,7 @@ pub struct StatusBadgeQuery { ) )] #[get("/v1/mods/{id}/status_badge")] -#[tracing::instrument(skip_all, err, fields(mod_id = %id))] +#[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn status_badge( data: web::Data, id: web::Path, diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 75652d84..eaf3ad40 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -105,7 +105,7 @@ async fn check_submission_lock( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_submission( path: web::Path, data: web::Data @@ -159,7 +159,7 @@ pub async fn get_submission( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/audit")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_submission_audit( path: web::Path, data: web::Data, @@ -204,7 +204,7 @@ pub async fn get_submission_audit( security(("bearer_token" = [])) )] #[put("v1/mods/{id}/versions/{version}/submission")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn update_submission( path: web::Path, data: web::Data, @@ -263,7 +263,7 @@ pub async fn update_submission( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/comments")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_comments( path: web::Path, data: web::Data, @@ -358,7 +358,7 @@ pub async fn get_comments( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/audit")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn get_comment_audit( path: web::Path, data: web::Data, @@ -410,7 +410,7 @@ pub async fn get_comment_audit( security(("bearer_token" = [])) )] #[post("v1/mods/{id}/versions/{version}/submission/comments")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn create_comment( path: web::Path, data: web::Data, @@ -493,7 +493,7 @@ pub async fn create_comment( security(("bearer_token" = [])) )] #[put("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn update_comment( path: web::Path, data: web::Data, @@ -593,7 +593,7 @@ pub async fn update_comment( security(("bearer_token" = [])) )] #[delete("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn delete_comment( path: web::Path, data: web::Data, @@ -679,7 +679,7 @@ pub async fn delete_comment( security(("bearer_token" = [])) )] #[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn get_attachments( path: web::Path, data: web::Data @@ -750,7 +750,7 @@ pub struct UploadAttachmentsForm { security(("bearer_token" = [])) )] #[post("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn upload_attachments( path: web::Path, data: web::Data, @@ -920,7 +920,7 @@ pub async fn upload_attachments( #[delete( "v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments/{attachment_id}" )] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id, attachment_id = %path.attachment_id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id, attachment_id = %path.attachment_id))] pub async fn delete_attachment( path: web::Path, data: web::Data, diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index e1a72d10..6160ff60 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -89,7 +89,7 @@ struct IndexQuery { ) )] #[get("v1/mods/{id}/versions")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn get_version_index( path: web::Path, data: web::Data, @@ -150,7 +150,7 @@ pub async fn get_version_index( ) )] #[get("v1/mods/{id}/versions/{version}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_one( path: web::Path, data: web::Data, @@ -213,7 +213,7 @@ struct DownloadQuery { ) )] #[get("v1/mods/{id}/versions/{version}/download")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn download_version( path: web::Path, data: web::Data, @@ -295,7 +295,7 @@ pub async fn download_version( ) )] #[post("v1/mods/{id}/versions")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path))] +#[tracing::instrument(skip_all, fields(mod_id = %path))] pub async fn create_version( path: web::Path, data: web::Data, @@ -501,7 +501,7 @@ pub async fn create_version( ) )] #[put("v1/mods/{id}/versions/{version}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path.id, version = %path.version))] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn update_version( path: web::Path, data: web::Data, diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index 50ac5661..db4d54e9 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -80,7 +80,7 @@ pub struct CreateQueryParams { ) )] #[get("/v1/mods")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn index( data: web::Data, query: web::Query, @@ -128,7 +128,7 @@ pub async fn index( ) )] #[get("/v1/mods/{id}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %id))] +#[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn get( data: web::Data, id: web::Path, @@ -203,7 +203,7 @@ pub async fn get( ) )] #[post("/v1/mods")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn create( data: web::Data, payload: web::Json, @@ -337,7 +337,7 @@ enum UpdateQueryResponse { ) )] #[get("/v1/mods/updates")] -#[tracing::instrument(skip_all, err, fields(ids = %query.ids, geode = %query.geode))] +#[tracing::instrument(skip_all, fields(ids = %query.ids, geode = %query.geode))] pub async fn get_mod_updates( data: web::Data, query: web::Query, @@ -398,7 +398,7 @@ pub async fn get_mod_updates( ) )] #[get("/v1/mods/{id}/logo")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path))] +#[tracing::instrument(skip_all, fields(mod_id = %path))] pub async fn get_logo( data: web::Data, path: web::Path, @@ -438,7 +438,7 @@ struct UpdateModPayload { ) )] #[put("/v1/mods/{id}")] -#[tracing::instrument(skip_all, err, fields(mod_id = %path))] +#[tracing::instrument(skip_all, fields(mod_id = %path))] pub async fn update_mod( data: web::Data, path: web::Path, diff --git a/src/endpoints/stats.rs b/src/endpoints/stats.rs index 982e0103..65553388 100644 --- a/src/endpoints/stats.rs +++ b/src/endpoints/stats.rs @@ -14,7 +14,7 @@ use crate::types::{api::ApiResponse, models::stats::Stats}; ) )] #[get("/v1/stats")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn get_stats(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; Ok(web::Json(ApiResponse { diff --git a/src/endpoints/tags.rs b/src/endpoints/tags.rs index 0d1c76e9..76352740 100644 --- a/src/endpoints/tags.rs +++ b/src/endpoints/tags.rs @@ -16,7 +16,7 @@ use crate::types::models::tag::Tag; ) )] #[get("/v1/tags")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn index(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; let tags = mod_tags::get_all_writable(&mut pool) @@ -41,7 +41,7 @@ pub async fn index(data: web::Data) -> Result ) )] #[get("/v1/detailed-tags")] -#[tracing::instrument(skip_all, err)] +#[tracing::instrument(skip_all)] pub async fn detailed_index(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; diff --git a/src/types/models/dependency.rs b/src/types/models/dependency.rs index fabd0437..18952078 100644 --- a/src/types/models/dependency.rs +++ b/src/types/models/dependency.rs @@ -108,7 +108,7 @@ pub enum DependencyImportance { } impl Dependency { - #[tracing::instrument(skip_all, err, fields(mod_version_ids = ?ids))] + #[tracing::instrument(skip_all, fields(mod_version_ids = ?ids))] pub async fn get_for_mod_versions( ids: &Vec, platform: Option, diff --git a/src/types/models/gd_version_alias.rs b/src/types/models/gd_version_alias.rs index d83353ed..1d906594 100644 --- a/src/types/models/gd_version_alias.rs +++ b/src/types/models/gd_version_alias.rs @@ -20,7 +20,7 @@ pub struct GDVersionAlias { } impl GDVersionAlias { - #[tracing::instrument(skip_all, err, fields(platform = ?platform, identifier = %identifier))] + #[tracing::instrument(skip_all, fields(platform = ?platform, identifier = %identifier))] pub async fn find( platform: VerPlatform, identifier: &str, diff --git a/src/types/models/incompatibility.rs b/src/types/models/incompatibility.rs index 7b6be4e7..c57b8cf2 100644 --- a/src/types/models/incompatibility.rs +++ b/src/types/models/incompatibility.rs @@ -91,7 +91,7 @@ impl FetchedIncompatibility { } impl Incompatibility { - #[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] + #[tracing::instrument(skip_all, fields(mod_version_id = %id))] pub async fn get_for_mod_version( id: i32, pool: &mut PgConnection, @@ -110,7 +110,7 @@ impl Incompatibility { .map_err(|e| e.into()) } - #[tracing::instrument(skip_all, err, fields(mod_version_ids = ?ids))] + #[tracing::instrument(skip_all, fields(mod_version_ids = ?ids))] pub async fn get_for_mod_versions( ids: &Vec, platform: Option, diff --git a/src/types/models/loader_version.rs b/src/types/models/loader_version.rs index c8940e4b..9f6862fb 100644 --- a/src/types/models/loader_version.rs +++ b/src/types/models/loader_version.rs @@ -77,7 +77,7 @@ impl LoaderVersionGetOne { } impl LoaderVersion { - #[tracing::instrument(skip_all, err, fields(gd = ?gd, platform = ?platform, accept_prereleases = %accept_prereleases))] + #[tracing::instrument(skip_all, fields(gd = ?gd, platform = ?platform, accept_prereleases = %accept_prereleases))] pub async fn get_latest( gd: Option, platform: Option, @@ -169,7 +169,7 @@ impl LoaderVersion { .map(|x| x.map(|y| y.into_loader_version())) } - #[tracing::instrument(skip_all, err, fields(tag = %tag))] + #[tracing::instrument(skip_all, fields(tag = %tag))] pub async fn get_one( tag: &str, pool: &mut PgConnection, @@ -189,7 +189,7 @@ impl LoaderVersion { .map(|x| x.map(|y| y.into_loader_version())) } - #[tracing::instrument(skip_all, err, fields(tag = %version.tag))] + #[tracing::instrument(skip_all, fields(tag = %version.tag))] pub async fn create_version( version: LoaderVersionCreate, pool: &mut PgConnection, @@ -213,7 +213,7 @@ impl LoaderVersion { .map_err(|e| e.into()) } - #[tracing::instrument(skip_all, err, fields(page = %page, per_page = %per_page))] + #[tracing::instrument(skip_all, fields(page = %page, per_page = %per_page))] pub async fn get_many( query: GetVersionsQuery, per_page: i64, diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index be286182..1aeac382 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -116,7 +116,7 @@ impl Mod { } } - #[tracing::instrument(skip_all, err)] + #[tracing::instrument(skip_all)] pub async fn get_stats(pool: &mut PgConnection) -> Result { let result = sqlx::query!( " @@ -149,7 +149,7 @@ impl Mod { } } - #[tracing::instrument(skip_all, err, fields(query = ?query.query, page = ?query.page, per_page = ?query.per_page))] + #[tracing::instrument(skip_all, fields(query = ?query.query, page = ?query.page, per_page = ?query.per_page))] pub async fn get_index( pool: &mut PgConnection, query: &IndexQueryParams, @@ -485,7 +485,7 @@ impl Mod { }) } - #[tracing::instrument(skip_all, err, fields(developer_id = %id, status = ?status, only_owner = %only_owner))] + #[tracing::instrument(skip_all, fields(developer_id = %id, status = ?status, only_owner = %only_owner))] pub async fn get_all_for_dev( id: i32, status: ModVersionStatusEnum, @@ -571,7 +571,7 @@ impl Mod { Ok(mods) } - #[tracing::instrument(skip_all, err, fields(mod_id = %id, only_accepted = %only_accepted))] + #[tracing::instrument(skip_all, fields(mod_id = %id, only_accepted = %only_accepted))] pub async fn get_one( id: &str, only_accepted: bool, @@ -669,7 +669,7 @@ impl Mod { /// At the moment this is only used to set the mod to featured. /// DOES NOT check if the mod exists - #[tracing::instrument(skip_all, err, fields(mod_id = %id, featured = %featured))] + #[tracing::instrument(skip_all, fields(mod_id = %id, featured = %featured))] pub async fn update_mod( id: &str, featured: bool, @@ -682,7 +682,7 @@ impl Mod { .map(|_| ()) } - #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids, platform = ?platforms, gd = ?gd))] + #[tracing::instrument(skip_all, fields(mod_ids = ?ids, platform = ?platforms, gd = ?gd))] pub async fn get_updates( ids: &[String], platforms: VerPlatform, diff --git a/src/types/models/mod_gd_version.rs b/src/types/models/mod_gd_version.rs index b00c07e6..fbd7bbd1 100644 --- a/src/types/models/mod_gd_version.rs +++ b/src/types/models/mod_gd_version.rs @@ -231,7 +231,7 @@ impl DetailedGDVersion { impl ModGDVersion { // to be used for GET mods/{id}/version/{version} - #[tracing::instrument(skip_all, err, fields(mod_version_id = %id))] + #[tracing::instrument(skip_all, fields(mod_version_id = %id))] pub async fn get_for_mod_version( id: i32, pool: &mut PgConnection, @@ -280,7 +280,7 @@ impl ModGDVersion { } // hello - #[tracing::instrument(skip_all, err, fields(mod_version_ids = ?versions))] + #[tracing::instrument(skip_all, fields(mod_version_ids = ?versions))] pub async fn get_for_mod_versions( versions: &[i32], pool: &mut PgConnection, diff --git a/src/types/models/mod_link.rs b/src/types/models/mod_link.rs index 4beab1e5..e2d0751b 100644 --- a/src/types/models/mod_link.rs +++ b/src/types/models/mod_link.rs @@ -14,7 +14,7 @@ pub struct ModLinks { } impl ModLinks { - #[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] + #[tracing::instrument(skip_all, fields(mod_id = %mod_id))] pub async fn fetch( mod_id: &str, pool: &mut PgConnection, @@ -32,7 +32,7 @@ impl ModLinks { .map_err(|e| e.into()) } - #[tracing::instrument(skip_all, err, fields(mod_ids = ?mod_ids))] + #[tracing::instrument(skip_all, fields(mod_ids = ?mod_ids))] pub async fn fetch_for_mods( mod_ids: &Vec, pool: &mut PgConnection, diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index cc939325..d78290f5 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -149,7 +149,7 @@ impl ModVersion { self.modify_download_link(app_url) } - #[tracing::instrument(skip_all, err, fields(mod_id = %query.mod_id, page = %query.page, per_page = %query.per_page))] + #[tracing::instrument(skip_all, fields(mod_id = %query.mod_id, page = %query.page, per_page = %query.per_page))] pub async fn get_index( query: IndexQuery, pool: &mut PgConnection, @@ -298,7 +298,7 @@ impl ModVersion { Ok(PaginatedData { data: ret, count }) } - #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids, gd = ?gd))] + #[tracing::instrument(skip_all, fields(mod_ids = ?ids, gd = ?gd))] pub async fn get_latest_for_mods( pool: &mut PgConnection, ids: &[String], @@ -379,7 +379,7 @@ impl ModVersion { }) } - #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids))] + #[tracing::instrument(skip_all, fields(mod_ids = ?ids))] pub async fn get_pending_for_mods( ids: &[String], pool: &mut PgConnection, @@ -415,7 +415,7 @@ impl ModVersion { Ok(ret) } - #[tracing::instrument(skip_all, err, fields(mod_id = %id, gd = ?gd))] + #[tracing::instrument(skip_all, fields(mod_id = %id, gd = ?gd))] pub async fn get_latest_for_mod( id: &str, gd: Option, @@ -504,7 +504,7 @@ impl ModVersion { Ok(Some(version)) } - #[tracing::instrument(skip_all, err, fields(mod_id = %id, version = %version))] + #[tracing::instrument(skip_all, fields(mod_id = %id, version = %version))] pub async fn get_one( id: &str, version: &str, @@ -561,7 +561,7 @@ impl ModVersion { Ok(Some(version)) } - #[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] + #[tracing::instrument(skip_all, fields(mod_id = %mod_id))] pub async fn get_accepted_count( mod_id: &str, pool: &mut PgConnection, diff --git a/src/types/models/stats.rs b/src/types/models/stats.rs index 585d6300..12e5110b 100644 --- a/src/types/models/stats.rs +++ b/src/types/models/stats.rs @@ -29,7 +29,7 @@ pub struct Stats { } impl Stats { - #[tracing::instrument(skip_all, err)] + #[tracing::instrument(skip_all)] pub async fn get_cached(pool: &mut PgConnection) -> Result { let mod_stats = Mod::get_stats(&mut *pool).await?; Ok(Stats { @@ -41,7 +41,7 @@ impl Stats { }) } - #[tracing::instrument(skip_all, err)] + #[tracing::instrument(skip_all)] async fn get_latest_github_release_download_count( pool: &mut PgConnection, ) -> Result { diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index ff57505e..3f6f3740 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -14,7 +14,7 @@ pub struct Tag { } impl Tag { - #[tracing::instrument(skip_all, err, fields(mod_id = %mod_id))] + #[tracing::instrument(skip_all, fields(mod_id = %mod_id))] pub async fn get_tags_for_mod( mod_id: &str, pool: &mut PgConnection, @@ -31,7 +31,7 @@ impl Tag { .map_err(|e| e.into()) } - #[tracing::instrument(skip_all, err, fields(mod_ids = ?ids))] + #[tracing::instrument(skip_all, fields(mod_ids = ?ids))] pub async fn get_tags_for_mods( ids: &Vec, pool: &mut PgConnection, @@ -57,7 +57,7 @@ impl Tag { Ok(ret) } - #[tracing::instrument(skip_all, err, fields(tags = %tags))] + #[tracing::instrument(skip_all, fields(tags = %tags))] pub async fn parse_tags(tags: &str, pool: &mut PgConnection) -> Result, ApiError> { let tags = tags .split(',') From d99bced37e47b79eea85ce44636f655c62362533 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 03:10:30 +0300 Subject: [PATCH 50/69] fix: actually read RUST_LOG / LOG_FORMAT --- src/logging.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logging.rs b/src/logging.rs index e057f589..f516d876 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -10,10 +10,10 @@ use tracing_subscriber::EnvFilter; /// exactly `"json"`, logs are emitted as JSON; otherwise the default /// compact/full text format is used. pub fn init() { - let filter = EnvFilter::try_from_default_env() + let filter = EnvFilter::try_from(dotenvy::var("RUST_LOG").unwrap_or("".into())) .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn,tracing_actix_web=info")); - let is_json = std::env::var("LOG_FORMAT").map(|v| v == "json").unwrap_or(false); + let is_json = dotenvy::var("LOG_FORMAT").map(|v| v == "json").unwrap_or(false); if is_json { tracing_subscriber::fmt() From 63a875060243355bd9ed4dc52acdec2f4f181bdf Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 03:20:03 +0300 Subject: [PATCH 51/69] fix: actually fix EnvFilter setup --- src/logging.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/logging.rs b/src/logging.rs index f516d876..63a816ce 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -10,8 +10,9 @@ use tracing_subscriber::EnvFilter; /// exactly `"json"`, logs are emitted as JSON; otherwise the default /// compact/full text format is used. pub fn init() { - let filter = EnvFilter::try_from(dotenvy::var("RUST_LOG").unwrap_or("".into())) - .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn,tracing_actix_web=info")); + const DEFAULT_VALUE: &str = "info,sqlx=warn,tracing_actix_web=info"; + + let filter = EnvFilter::from(dotenvy::var("RUST_LOG").unwrap_or(DEFAULT_VALUE.into())); let is_json = dotenvy::var("LOG_FORMAT").map(|v| v == "json").unwrap_or(false); From bb5ec1daeb772464b4303310cf4f1b98165fe75c Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 03:30:53 +0300 Subject: [PATCH 52/69] fix: simplify if let chain --- src/endpoints/mods.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index db4d54e9..06ba35d9 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -464,10 +464,9 @@ pub async fn update_mod( if featured != payload.featured { let item = Mod::get_one(&id, true, &mut pool).await?; - if let Some(item) = item { - if let Some(owner) = developers::get_owner_for_mod(&id, &mut pool).await? { - let first_ver = item.versions.first(); - if let Some(ver) = first_ver { + if let Some(item) = item + && let Some(owner) = developers::get_owner_for_mod(&id, &mut pool).await? + && let Some(ver) = item.versions.first() { ModFeaturedEvent { id: item.id, name: ver.name.clone(), @@ -479,8 +478,6 @@ pub async fn update_mod( .to_discord_webhook() .send(data.webhook_url()); } - } - } } Ok(HttpResponse::NoContent()) From f97b35b2256095f9c817c32fde7a3578b8a9faf9 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 03:43:08 +0300 Subject: [PATCH 53/69] chore: bump all dependencies --- Cargo.lock | 1302 ++++++++++++++++++++-------------------------------- Cargo.toml | 6 +- 2 files changed, 495 insertions(+), 813 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97149804..79ce11d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" dependencies = [ "actix-utils", "actix-web", - "derive_more 2.1.1", + "derive_more", "futures-util", "log", "once_cell", @@ -46,7 +46,7 @@ dependencies = [ "actix-web", "bitflags", "bytes", - "derive_more 2.1.1", + "derive_more", "futures-core", "http-range", "log", @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.12.1" +version = "3.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662" +checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" dependencies = [ "actix-codec", "actix-rt", @@ -72,10 +72,10 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more 2.1.1", + "derive_more", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.2.0", "futures-core", "h2", "http 0.2.12", @@ -87,7 +87,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.10.1", + "rand 0.10.2", "sha1 0.11.0", "smallvec", "tokio", @@ -108,14 +108,14 @@ dependencies = [ [[package]] name = "actix-multipart" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +checksum = "560e3dd4eae03837f86d1b6bf6222c508568eff36845ef5ebb3a0dff480e3f64" dependencies = [ "actix-multipart-derive", "actix-utils", "actix-web", - "derive_more 0.99.20", + "derive_more", "futures-core", "futures-util", "httparse", @@ -123,7 +123,7 @@ dependencies = [ "log", "memchr", "mime", - "rand 0.8.6", + "rand 0.10.2", "serde", "serde_json", "serde_plain", @@ -133,12 +133,12 @@ dependencies = [ [[package]] name = "actix-multipart-derive" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +checksum = "8720bceaa6797fd8b2deab968d52e1120b2a8c30950939f6c8cdb42a910bc885" dependencies = [ + "bytesize", "darling", - "parse-size", "proc-macro2", "quote", "syn", @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.13.0" +version = "4.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" dependencies = [ "actix-codec", "actix-http", @@ -225,9 +225,9 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more 2.1.1", + "derive_more", "encoding_rs", - "foldhash", + "foldhash 0.2.0", "futures-core", "futures-util", "impl-more", @@ -243,7 +243,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.3", + "socket2 0.6.4", "time", "tracing", "url", @@ -269,13 +269,13 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpubits", + "cpufeatures 0.3.0", ] [[package]] @@ -313,9 +313,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -328,14 +328,13 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" +checksum = "68b9d3370580a12f4b7a10fdcc18b28942c083ba570e3d954fe59d10951b85a2" dependencies = [ "cssparser", "html5ever", "maplit", - "tendril", "url", ] @@ -400,9 +399,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -426,9 +425,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "as-slice" @@ -490,9 +489,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "av-scenechange" @@ -509,7 +508,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.18", + "thiserror", "v_frame", "y4m", ] @@ -530,18 +529,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" dependencies = [ "arrayvec", ] [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -549,14 +548,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -579,9 +579,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -606,18 +606,19 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", + "zeroize", ] [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -626,9 +627,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -636,21 +637,21 @@ dependencies = [ [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" [[package]] name = "byteorder" @@ -666,15 +667,21 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytesize" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] @@ -690,9 +697,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -700,12 +707,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -720,9 +721,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -731,9 +732,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -745,11 +746,11 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.1.7", + "crypto-common 0.2.2", "inout", ] @@ -802,6 +803,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "color_quant" version = "1.1.0" @@ -864,15 +871,9 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "convert_case" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "convert_case" @@ -910,6 +911,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -930,9 +937,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -954,18 +961,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -973,27 +980,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -1013,41 +1020,38 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] [[package]] name = "cssparser" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" dependencies = [ - "cssparser-macros", "dtoa-short", "itoa", - "phf", "smallvec", ] [[package]] -name = "cssparser-macros" -version = "0.6.1" +name = "ctutils" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ - "quote", - "syn", + "cmov", ] [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -1055,11 +1059,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -1069,9 +1072,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -1100,9 +1103,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_arbitrary" @@ -1115,19 +1115,6 @@ dependencies = [ "syn", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -1143,7 +1130,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1165,20 +1152,22 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", - "crypto-common 0.2.1", + "crypto-common 0.2.2", + "ctutils", + "zeroize", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1214,9 +1203,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] @@ -1300,14 +1289,16 @@ dependencies = [ [[package]] name = "exr" -version = "1.74.0" +version = "1.74.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide", + "num-complex", + "pulp", "rayon-core", "smallvec", "zune-inflate", @@ -1321,23 +1312,9 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -1388,6 +1365,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1403,16 +1386,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.32" @@ -1547,7 +1520,7 @@ dependencies = [ "serde_json", "sha256", "sqlx", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "tracing-actix-web", @@ -1557,7 +1530,7 @@ dependencies = [ "utoipa-swagger-ui", "uuid", "validator", - "zip 7.2.0", + "zip 8.6.0", ] [[package]] @@ -1580,25 +1553,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", + "wasm-bindgen", ] [[package]] @@ -1649,14 +1620,14 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -1685,7 +1656,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1697,6 +1668,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "home" version = "0.5.12" @@ -1708,13 +1688,12 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.35.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" dependencies = [ "log", "markup5ever", - "match_token", ] [[package]] @@ -1730,9 +1709,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1745,7 +1724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.2", ] [[package]] @@ -1756,7 +1735,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body", "pin-project-lite", ] @@ -1781,24 +1760,24 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body", "httparse", "itoa", @@ -1814,7 +1793,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", + "http 1.4.2", "hyper", "hyper-util", "rustls", @@ -1833,14 +1812,14 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -1952,12 +1931,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1977,9 +1950,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2021,15 +1994,15 @@ dependencies = [ [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" [[package]] name = "impl-more" -version = "0.1.9" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" [[package]] name = "indexmap" @@ -2038,18 +2011,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -2078,16 +2051,6 @@ dependencies = [ "serde", ] -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2111,27 +2074,32 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys 0.3.1", + "jni-macros", + "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror", "walkdir", - "windows-sys 0.45.0", + "windows-link", ] [[package]] -name = "jni-sys" -version = "0.3.1" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "jni-sys 0.4.1", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] @@ -2155,23 +2123,22 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -2190,12 +2157,6 @@ dependencies = [ "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lebe" version = "0.5.3" @@ -2204,9 +2165,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libbz2-rs-sys" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" @@ -2216,9 +2177,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" dependencies = [ "arbitrary", "cc", @@ -2232,14 +2193,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.7.4", + "redox_syscall 0.9.0", ] [[package]] @@ -2292,9 +2253,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "loop9" @@ -2313,20 +2274,13 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rust2" -version = "0.15.7" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" dependencies = [ - "crc", - "sha2", + "sha2 0.11.0", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "maplit" version = "1.0.2" @@ -2335,26 +2289,15 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.35.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" dependencies = [ "log", "tendril", "web_atoms", ] -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "matchers" version = "0.2.0" @@ -2386,9 +2329,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mime" @@ -2418,9 +2361,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -2472,9 +2415,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "no_std_io2" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" dependencies = [ "memchr", ] @@ -2505,9 +2448,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2529,11 +2472,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -2557,11 +2510,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2634,12 +2586,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "parse-size" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" - [[package]] name = "paste" version = "1.0.15" @@ -2654,12 +2600,12 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pbkdf2" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.10.7", - "hmac", + "digest 0.11.3", + "hmac 0.13.0", ] [[package]] @@ -2679,19 +2625,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros", "phf_shared", + "serde", ] [[package]] name = "phf_codegen" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ "phf_generator", "phf_shared", @@ -2699,32 +2645,19 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.6", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "phf_generator", + "fastrand", "phf_shared", - "proc-macro2", - "quote", - "syn", ] [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher", ] @@ -2844,32 +2777,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" +name = "proc-macro-error-attr3" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +checksum = "34e4dd828515431dd6c4a030d26f7eaed7dd4778226e9d2bb968d65ca4ec3d4d" dependencies = [ "proc-macro2", "quote", ] [[package]] -name = "proc-macro-error2" -version = "2.0.1" +name = "proc-macro-error3" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +checksum = "5ee475e440453418ff1335189eddf7101ba502cd818ab7ae04209bc83aa925aa" dependencies = [ - "proc-macro-error-attr2", + "proc-macro-error-attr3", "proc-macro2", "quote", "syn", @@ -2886,28 +2809,51 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", "syn", ] +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "pxfm" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "qoi" @@ -2926,9 +2872,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -2937,8 +2883,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", - "thiserror 2.0.18", + "socket2 0.6.4", + "thiserror", "tokio", "tracing", "web-time", @@ -2946,21 +2892,22 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -2968,23 +2915,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.6.4", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -3024,12 +2971,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -3077,6 +3024,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rav1e" version = "0.8.1" @@ -3107,7 +3063,7 @@ dependencies = [ "rand 0.9.4", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.18", + "thiserror", "v_frame", "wasm-bindgen", ] @@ -3127,6 +3083,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + [[package]] name = "rayon" version = "1.12.0" @@ -3147,6 +3112,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3158,18 +3129,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", @@ -3179,9 +3150,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" dependencies = [ "aho-corasick", "memchr", @@ -3196,20 +3167,20 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body", "http-body-util", "hyper", @@ -3280,9 +3251,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.11.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -3291,10 +3262,11 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.11.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" dependencies = [ + "mime_guess", "proc-macro2", "quote", "rust-embed-utils", @@ -3304,19 +3276,19 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.11.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" dependencies = [ - "sha2", + "sha2 0.11.0", "walkdir", ] [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3342,9 +3314,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", @@ -3356,9 +3328,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3368,9 +3340,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -3378,9 +3350,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", @@ -3417,9 +3389,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -3512,9 +3484,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3546,9 +3518,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -3563,7 +3535,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -3577,6 +3549,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha256" version = "1.6.0" @@ -3586,7 +3569,7 @@ dependencies = [ "async-trait", "bytes", "hex", - "sha2", + "sha2 0.10.9", "tokio", ] @@ -3601,9 +3584,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -3631,6 +3614,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -3640,11 +3633,17 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3654,9 +3653,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -3673,9 +3672,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3740,9 +3739,9 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "tracing", @@ -3778,7 +3777,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -3811,7 +3810,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", "md-5", @@ -3821,12 +3820,12 @@ dependencies = [ "rand 0.8.6", "rsa", "serde", - "sha1 0.10.6", - "sha2", + "sha1 0.10.7", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror", "tracing", "uuid", "whoami", @@ -3851,7 +3850,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "ipnetwork", "itoa", @@ -3862,11 +3861,11 @@ dependencies = [ "rand 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror", "tracing", "uuid", "whoami", @@ -3892,7 +3891,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", + "thiserror", "tracing", "url", "uuid", @@ -3906,22 +3905,21 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared", "precomputed-hash", - "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ "phf_generator", "phf_shared", @@ -3954,9 +3952,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -3996,7 +3994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4004,22 +4002,11 @@ dependencies = [ [[package]] name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "thiserror" -version = "1.0.69" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ - "thiserror-impl 1.0.69", + "new_debug_unreachable", ] [[package]] @@ -4028,18 +4015,7 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -4078,12 +4054,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "js-sys", "num-conv", "powerfmt", @@ -4094,15 +4069,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -4120,9 +4095,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4135,9 +4110,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -4145,7 +4120,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.4", "tokio-macros", "windows-sys 0.61.2", ] @@ -4212,25 +4187,25 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", "bitflags", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", "http-body-util", - "iri-string", "pin-project-lite", "tokio", "tokio-util", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4347,9 +4322,9 @@ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicase" @@ -4386,9 +4361,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -4420,12 +4395,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4440,9 +4409,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" dependencies = [ "indexmap", "serde", @@ -4452,9 +4421,9 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" dependencies = [ "proc-macro2", "quote", @@ -4483,13 +4452,13 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", - "rand 0.10.1", + "rand 0.10.2", "serde_core", "wasm-bindgen", ] @@ -4529,13 +4498,12 @@ dependencies = [ [[package]] name = "validator_derive" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +checksum = "240e4b81c20a1d6d50d1d7265c658dfbd204e8b9ac4d80f3c931f39462196335" dependencies = [ "darling", - "once_cell", - "proc-macro-error2", + "proc-macro-error3", "proc-macro2", "quote", "syn", @@ -4586,20 +4554,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -4610,9 +4569,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4623,9 +4582,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4633,9 +4592,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4643,9 +4602,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -4656,52 +4615,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -4719,9 +4644,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.1.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ "phf", "phf_codegen", @@ -4731,9 +4656,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -4822,15 +4747,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -4849,15 +4765,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -4867,21 +4774,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -4906,36 +4798,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4948,18 +4817,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4972,18 +4829,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4996,30 +4841,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5032,18 +4859,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -5056,18 +4871,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -5080,18 +4883,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -5104,106 +4895,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -5218,9 +4915,9 @@ checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5241,18 +4938,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", @@ -5261,9 +4958,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -5282,23 +4979,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -5349,9 +5032,9 @@ dependencies = [ [[package]] name = "zip" -version = "7.2.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "aes", "bzip2", @@ -5359,15 +5042,14 @@ dependencies = [ "crc32fast", "deflate64", "flate2", - "generic-array", - "getrandom 0.3.4", - "hmac", + "getrandom 0.4.3", + "hmac 0.13.0", "indexmap", "lzma-rust2", "memchr", "pbkdf2", "ppmd-rust", - "sha1 0.10.6", + "sha1 0.11.0", "time", "typed-path", "zeroize", @@ -5377,9 +5059,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index c6bb3c98..4e60b745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] image = { version = "0.25", features = ["png", "jpeg", "webp"] } -actix-multipart = "0.7" +actix-multipart = "0.8" actix-web = "4.10" anyhow = "1.0" dotenvy = "0.15" @@ -22,9 +22,9 @@ sqlx = { version = "0.8", features = [ tokio = { version = "1.44", features = ["rt", "macros", "rt-multi-thread"] } reqwest = { version = "0.13", default-features = false, features = ["json", "gzip", "rustls", "query"] } uuid = { version = "1.8", features = ["v4", "fast-rng", "macro-diagnostics"] } -zip = { version = "7.2.0" } +zip = "8.6.0" # Required for zip crate to compile properly -lzma-rust2 = "0.15.7" +lzma-rust2 = "0.16.5" sha256 = "1.5" semver = "1.0" clap = { version = "4.5", features = ["derive"] } From 93ebd79a5a832f2c283862814353d0e9bd3e5337 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 03:44:38 +0300 Subject: [PATCH 54/69] chore: version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79ce11d8..19cbdcb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1497,7 +1497,7 @@ dependencies = [ [[package]] name = "geode-index" -version = "0.54.1" +version = "0.55.0" dependencies = [ "actix-cors", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 4e60b745..16a86afd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geode-index" -version = "0.54.1" +version = "0.55.0" edition = "2024" [dependencies] From 464f5ba95b475cc0e49c2e5af5af03017660b78e Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 04:03:26 +0300 Subject: [PATCH 55/69] fix: set tracing_actix_web level to error info and warn log every single 404, not good --- docs/dev_setup.md | 2 +- src/logging.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev_setup.md b/docs/dev_setup.md index 829b1a23..5b58a33a 100644 --- a/docs/dev_setup.md +++ b/docs/dev_setup.md @@ -70,7 +70,7 @@ Third, we need to setup a local GitHub OAuth app. Since the index doesn't store ## 4. Logging Logging is configured via environment variables. See the `.env.example` file for the available options: -- `RUST_LOG`: Controls log verbosity (e.g., "info", "debug", "info,sqlx=warn", "geode_index=debug"). Defaults to "info,sqlx=warn,tracing_actix_web=info" when unset. +- `RUST_LOG`: Controls log verbosity (e.g., "info", "debug", "info,sqlx=warn", "geode_index=debug"). Defaults to "info,sqlx=warn,tracing_actix_web=error" when unset. - `LOG_FORMAT`: Output format - "text" (default, human-readable) or "json" (structured, for log aggregation). After all of this is done, you should be able to run `cargo run` inside the index directory. The migrations will be ran automatically, and the index will start. You can check `http://localhost:8080` (if you haven't changed the port in your .env file) to see if it all works. diff --git a/src/logging.rs b/src/logging.rs index 63a816ce..b01615f9 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -3,14 +3,14 @@ use tracing_subscriber::EnvFilter; /// Initialize the global `tracing` subscriber. /// /// The filter is controlled by the `RUST_LOG` env var, falling back to -/// `"info,sqlx=warn,tracing_actix_web=info"` when unset. This default is +/// `"info,sqlx=warn,tracing_actix_web=error"` when unset. This default is /// identical for debug and release builds. /// /// The output format is controlled by the `LOG_FORMAT` env var: when it is /// exactly `"json"`, logs are emitted as JSON; otherwise the default /// compact/full text format is used. pub fn init() { - const DEFAULT_VALUE: &str = "info,sqlx=warn,tracing_actix_web=info"; + const DEFAULT_VALUE: &str = "info,sqlx=warn,tracing_actix_web=error"; let filter = EnvFilter::from(dotenvy::var("RUST_LOG").unwrap_or(DEFAULT_VALUE.into())); From cd8e97c7a2315fcece126478e28186dfaf039f53 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 14:32:16 +0300 Subject: [PATCH 56/69] fix: add acquire_timeout for pgpool --- src/config.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config.rs b/src/config.rs index af719f31..9ba092e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,8 @@ use std::time::Duration; use moka::future::Cache; +use sqlx::ConnectOptions; +use sqlx::postgres::PgConnectOptions; use crate::storage::{PrivateStorage, PublicStorage, StaticStorage}; use crate::{ @@ -44,6 +46,7 @@ pub async fn build_config() -> anyhow::Result { let pool = sqlx::postgres::PgPoolOptions::default() .max_connections(pg_connections) + .acquire_timeout(Duration::from_secs(10)) .connect(&env_url) .await?; let port = dotenvy::var("PORT").map_or(8080, |x: String| x.parse::().unwrap()); From 0080d5b9ed3eda78248227758c1c1b53257e0035 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 14:53:01 +0300 Subject: [PATCH 57/69] fix: remove some unused imports --- src/config.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9ba092e2..477357af 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,6 @@ use std::time::Duration; use moka::future::Cache; -use sqlx::ConnectOptions; -use sqlx::postgres::PgConnectOptions; use crate::storage::{PrivateStorage, PublicStorage, StaticStorage}; use crate::{ From 6738a7258c0945f4319eea1a3b215ca21c7e08f0 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 15:19:08 +0300 Subject: [PATCH 58/69] chore: add LICENSE.txt --- LICENSE.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..127a5bc3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file From f9b9d01a84f71b458c9cf6c8f62a319527c11f0a Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 15:19:18 +0300 Subject: [PATCH 59/69] chore: version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19cbdcb5..cab1f242 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1497,7 +1497,7 @@ dependencies = [ [[package]] name = "geode-index" -version = "0.55.0" +version = "0.55.1" dependencies = [ "actix-cors", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 16a86afd..18762e26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geode-index" -version = "0.55.0" +version = "0.55.1" edition = "2024" [dependencies] From f2f59a18c917c51855a7e80e90ca5a5737a03f27 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 15:29:42 +0300 Subject: [PATCH 60/69] chore: include static files in docker image --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3410d0d7..c785ef57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -88,9 +88,9 @@ RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations +COPY storage/static ./storage/static RUN addgroup -S -g 1000 geode && adduser -S -u 1000 geode -G geode \ - && mkdir -p storage \ && chown -R geode:geode /app USER geode @@ -109,9 +109,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations +COPY storage/static ./storage/static RUN groupadd --system --gid 1000 geode && useradd --system --uid 1000 --gid geode geode \ - && mkdir -p storage \ && chown -R geode:geode /app USER geode From d530d5e6023c41bbb92e539f15a08369406386a9 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 15:31:22 +0300 Subject: [PATCH 61/69] chore: update deploy script to use docker --- .github/workflows/deploy.yaml | 48 +++++------------------------------ 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3b0a7b18..e0909803 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,57 +1,23 @@ name: Deploy Index on: - release: - types: [published] + workflow_run: + workflows: ["Publish Docker Image"] + types: [completed] workflow_dispatch: jobs: deploy: + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-24.04 steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Get release tag - id: get_tag - run: | - if [ "${{ github.event_name }}" == "release" ]; then - echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT - else - # For workflow_dispatch, get the latest release tag - echo "tag=$(gh release list --limit 1 --json tagName --jq '.[0].tagName')" >> $GITHUB_OUTPUT - fi - env: - GH_TOKEN: ${{ github.token }} - - - name: Download release assets - run: | - mkdir -p ./artifacts/x86_64 ./artifacts/aarch64 - gh release download ${{ steps.get_tag.outputs.tag }} \ - --pattern "geode-index-${{ steps.get_tag.outputs.tag }}-linux-x86_64" \ - --dir ./artifacts/x86_64 - gh release download ${{ steps.get_tag.outputs.tag }} \ - --pattern "geode-index-${{ steps.get_tag.outputs.tag }}-linux-aarch64" \ - --dir ./artifacts/aarch64 - chmod +x ./artifacts/x86_64/geode-index-${{ steps.get_tag.outputs.tag }}-linux-x86_64 - chmod +x ./artifacts/aarch64/geode-index-${{ steps.get_tag.outputs.tag }}-linux-aarch64 - env: - GH_TOKEN: ${{ github.token }} - - name: Create ssh key run: | install -m 600 -D /dev/null ~/.ssh/id_rsa echo "${{ secrets.PRIVATE_KEY }}" > ~/.ssh/id_rsa ssh-keyscan -p ${{ secrets.INDEX_SSH_PORT }} -H ${{ secrets.INDEX_SERVER }} > ~/.ssh/known_hosts - - name: Upload target to server - id: upload - run: | - rsync -avz -e "ssh -p ${{ secrets.INDEX_SSH_PORT }} -i ~/.ssh/id_rsa" --owner --group --chmod=775 --chown=${{ secrets.INDEX_RUN_USER }}:${{ secrets.INDEX_RUN_GROUP }} ./artifacts/${{ secrets.INDEX_DEPLOY_ARCH }}/geode-index-${{ steps.get_tag.outputs.tag }}-linux-${{ secrets.INDEX_DEPLOY_ARCH }} ${{ secrets.INDEX_USER }}@${{ secrets.INDEX_SERVER }}:${{ secrets.INDEX_COPY_LOCATION }} - rsync -avz -e "ssh -p ${{ secrets.INDEX_SSH_PORT }} -i ~/.ssh/id_rsa" --owner --group --chmod=775 --chown=${{ secrets.INDEX_RUN_USER }}:${{ secrets.INDEX_RUN_GROUP }} ./migrations/* ${{ secrets.INDEX_USER }}@${{ secrets.INDEX_SERVER }}:${{ secrets.MIGRATIONS_COPY_LOCATION }} - rsync -avz -e "ssh -p ${{ secrets.INDEX_SSH_PORT }} -i ~/.ssh/id_rsa" --owner --group --chmod=775 --chown=${{ secrets.INDEX_RUN_USER }}:${{ secrets.INDEX_RUN_GROUP }} ./storage/static ${{ secrets.INDEX_USER }}@${{ secrets.INDEX_SERVER }}:${{ secrets.STORAGE_STATIC_COPY_LOCATION }} - - - name: Run update script + - name: Deploy run: | - ssh -p ${{ secrets.INDEX_SSH_PORT }} ${{ secrets.INDEX_USER }}@${{ secrets.INDEX_SERVER }} "cd ${{ secrets.INDEX_UPDATE_SCRIPT_PATH }} && ./update.sh" - if: steps.upload.outcome == 'success' + ssh -p ${{ secrets.INDEX_SSH_PORT }} -i ~/.ssh/id_rsa ${{ secrets.INDEX_USER }}@${{ secrets.INDEX_SERVER }} \ + "cd ${{ secrets.INDEX_DIR }} && docker compose pull && docker compose up -d --wait" From d884f2aab99fc31238926089616cb6801b0285df Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 16:00:13 +0300 Subject: [PATCH 62/69] fix: separate static from storage --- Dockerfile | 8 +++++-- README.md | 6 +++-- docker/sync-static.sh | 7 ++++++ docs/examples/docker-compose.example.yml | 24 +++++++++++++++---- docs/examples/nginx.example.conf | 4 ++-- src/main.rs | 2 +- src/storage.rs | 2 +- .../shields/mod_downloads.svg | 0 .../shields/mod_gd_version.svg | 0 .../shields/mod_geode_version.svg | 0 .../static => static}/shields/mod_version.svg | 0 11 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 docker/sync-static.sh rename {storage/static => static}/shields/mod_downloads.svg (100%) rename {storage/static => static}/shields/mod_gd_version.svg (100%) rename {storage/static => static}/shields/mod_geode_version.svg (100%) rename {storage/static => static}/shields/mod_version.svg (100%) diff --git a/Dockerfile b/Dockerfile index c785ef57..3013b2e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -88,9 +88,11 @@ RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations -COPY storage/static ./storage/static +COPY static ./static +COPY docker/sync-static.sh /usr/local/bin/sync-static.sh RUN addgroup -S -g 1000 geode && adduser -S -u 1000 geode -G geode \ + && chmod +x /usr/local/bin/sync-static.sh \ && chown -R geode:geode /app USER geode @@ -109,9 +111,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app COPY --from=builder /app/geode-index ./geode-index COPY migrations ./migrations -COPY storage/static ./storage/static +COPY static ./static +COPY docker/sync-static.sh /usr/local/bin/sync-static.sh RUN groupadd --system --gid 1000 geode && useradd --system --uid 1000 --gid geode geode \ + && chmod +x /usr/local/bin/sync-static.sh \ && chown -R geode:geode /app USER geode diff --git a/README.md b/README.md index e4766530..97b8e35e 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,12 @@ The image has a built-in `HEALTHCHECK` that polls `GET /` (expects `200`); the s ### Persisting uploaded files -The server writes uploaded files to `/app/storage` inside the container (static assets, public downloads, private files). This is **not** persisted by default, if you remove/recreate the container without a volume, uploads are lost. +The server writes uploaded files to `/app/storage` inside the container (public downloads, private files). This is **not** persisted by default, if you remove/recreate the container without a volume, uploads are lost. + +Static assets (`/app/static`) are baked into the image itself and don't need persisting, they are re-synced to whatever serves them whenever the image updates, see the Compose example below. > [!IMPORTANT] -> The index does not serve these files itself in production. You are responsible for serving them with your own reverse proxy: `/app/storage/static` must be served at `/static/`, and `/app/storage/public` at `/storage/`, both relative to `APP_URL`. +> The index does not serve these files itself in production. You are responsible for serving them with your own reverse proxy: `/app/static` must be served at `/static/`, and `/app/storage/public` at `/storage/`, both relative to `APP_URL`. The container runs as a fixed non-root user, UID `1000`, GID `1000`. diff --git a/docker/sync-static.sh b/docker/sync-static.sh new file mode 100644 index 00000000..60483d4c --- /dev/null +++ b/docker/sync-static.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +target="${1:-/shared-static}" + +rm -rf "${target:?}"/* "${target}"/.[!.]* 2>/dev/null || true +cp -r /app/static/. "$target" \ No newline at end of file diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index b82878c0..843696c1 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -4,6 +4,15 @@ # "Running with Docker" section of the README for environment variables. services: + # Re-syncs static assets from whichever image tag is currently pulled into + # the geode-static volume, so nginx always serves assets matching the + # deployed (or rolled-back) app version. Runs once per `docker compose up`. + static-init: + image: ghcr.io/geode-sdk/server:latest-alpine + entrypoint: ["/usr/local/bin/sync-static.sh", "/shared-static"] + volumes: + - geode-static:/shared-static + app: image: ghcr.io/geode-sdk/server:latest-alpine restart: unless-stopped @@ -13,7 +22,8 @@ services: APP_URL: https://api.sapphire-sdk.org FRONT_URL: https://sapphire-sdk.org volumes: - - geode-storage:/app/storage + - geode-storage-public:/app/storage/public + - geode-storage-private:/app/storage/private networks: - geode @@ -21,9 +31,13 @@ services: image: nginx:1.27-alpine restart: unless-stopped depends_on: - - app + app: + condition: service_started + static-init: + condition: service_completed_successfully volumes: - - geode-storage:/app/storage:ro + - geode-static:/app/static:ro + - geode-storage-public:/app/storage/public:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro # include only if you want to make this available on the host machine # (if your reverse proxy is on the host, not inside docker) @@ -33,7 +47,9 @@ services: - geode volumes: - geode-storage: + geode-static: + geode-storage-public: + geode-storage-private: networks: geode: diff --git a/docs/examples/nginx.example.conf b/docs/examples/nginx.example.conf index 661056ff..595080a7 100644 --- a/docs/examples/nginx.example.conf +++ b/docs/examples/nginx.example.conf @@ -6,14 +6,14 @@ # reverse proxy. # # Paths must match src/main.rs's actix_files mounts: -# /static -> storage/static +# /static -> static # /storage -> storage/public server { listen 80; location /static/ { - alias /app/storage/static/; + alias /app/static/; } location /storage/ { diff --git a/src/main.rs b/src/main.rs index 37753912..e15082c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,7 @@ async fn main() -> anyhow::Result<()> { #[cfg(feature = "dev-tools")] let app = app - .service(actix_files::Files::new("/static", "storage/static")) + .service(actix_files::Files::new("/static", "static")) .service(actix_files::Files::new("/storage", "storage/public")); app diff --git a/src/storage.rs b/src/storage.rs index 475b04a4..83fda493 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -9,7 +9,7 @@ pub struct StaticStorage { impl StaticStorage { pub fn new(app_url: String) -> Self { Self { - base_path: PathBuf::from("storage/static"), + base_path: PathBuf::from("static"), app_url } } diff --git a/storage/static/shields/mod_downloads.svg b/static/shields/mod_downloads.svg similarity index 100% rename from storage/static/shields/mod_downloads.svg rename to static/shields/mod_downloads.svg diff --git a/storage/static/shields/mod_gd_version.svg b/static/shields/mod_gd_version.svg similarity index 100% rename from storage/static/shields/mod_gd_version.svg rename to static/shields/mod_gd_version.svg diff --git a/storage/static/shields/mod_geode_version.svg b/static/shields/mod_geode_version.svg similarity index 100% rename from storage/static/shields/mod_geode_version.svg rename to static/shields/mod_geode_version.svg diff --git a/storage/static/shields/mod_version.svg b/static/shields/mod_version.svg similarity index 100% rename from storage/static/shields/mod_version.svg rename to static/shields/mod_version.svg From 2e76679e88f81984a59aa6705febaf060c065ba5 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 16:04:27 +0300 Subject: [PATCH 63/69] chore: add postgres to docker-compose example --- docs/examples/docker-compose.example.yml | 27 ++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index 843696c1..e9b06f45 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -1,4 +1,4 @@ -# Example production setup: app + nginx sharing a storage volume. +# Example production setup: app + nginx + postgres sharing a storage volume. # # See nginx.example.conf for the corresponding nginx config, and the # "Running with Docker" section of the README for environment variables. @@ -16,8 +16,11 @@ services: app: image: ghcr.io/geode-sdk/server:latest-alpine restart: unless-stopped + depends_on: + postgres: + condition: service_healthy environment: - DATABASE_URL: postgres://user:password@host/schema + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${POSTGRES_DB} PORT: "3000" APP_URL: https://api.sapphire-sdk.org FRONT_URL: https://sapphire-sdk.org @@ -46,7 +49,27 @@ services: networks: - geode + postgres: + image: postgres:17-alpine + restart: unless-stopped + # you should probably not keep those variables inside this + # compose file, maybe define a .env file for it + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - geode-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - geode + volumes: + geode-postgres: geode-static: geode-storage-public: geode-storage-private: From 19ca5a3d62924449bd1c6e49c73c5e11067b0209 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sat, 11 Jul 2026 16:43:03 +0300 Subject: [PATCH 64/69] fix: make sure static-init actually completes --- docker/sync-static.sh | 3 ++- docs/examples/docker-compose.example.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/sync-static.sh b/docker/sync-static.sh index 60483d4c..210de7f3 100644 --- a/docker/sync-static.sh +++ b/docker/sync-static.sh @@ -4,4 +4,5 @@ set -eu target="${1:-/shared-static}" rm -rf "${target:?}"/* "${target}"/.[!.]* 2>/dev/null || true -cp -r /app/static/. "$target" \ No newline at end of file +cp -r /app/static/. "$target" +chmod -R a+rX "$target" \ No newline at end of file diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index e9b06f45..1110e701 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -10,6 +10,7 @@ services: static-init: image: ghcr.io/geode-sdk/server:latest-alpine entrypoint: ["/usr/local/bin/sync-static.sh", "/shared-static"] + user: "0:0" volumes: - geode-static:/shared-static From f49457aad3cef39ebc145ee9b461ed6fbca24345 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 12 Jul 2026 15:32:52 +0300 Subject: [PATCH 65/69] fix: map BadRequest to code 400 instead of 500 --- src/endpoints/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index c1e66e20..d20001d6 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -65,6 +65,7 @@ impl actix_web::ResponseError for ApiError { ApiError::Json(..) => StatusCode::BAD_REQUEST, ApiError::TooManyRequests(..) => StatusCode::TOO_MANY_REQUESTS, ApiError::NotFound(_) => StatusCode::NOT_FOUND, + ApiError::BadRequest(..) => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR, } } From 0b3e0ecd74c61eeee48c1f1fe7b09ec581a3ed6b Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 12 Jul 2026 15:44:43 +0300 Subject: [PATCH 66/69] fix: simplify actix error logging - remove the extra tracing::error! call made for error_response - only filter tracing_actix_web::middleware to error --- docs/dev_setup.md | 2 +- src/endpoints/mod.rs | 3 --- src/logging.rs | 6 +++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/dev_setup.md b/docs/dev_setup.md index 5b58a33a..ddd54a22 100644 --- a/docs/dev_setup.md +++ b/docs/dev_setup.md @@ -70,7 +70,7 @@ Third, we need to setup a local GitHub OAuth app. Since the index doesn't store ## 4. Logging Logging is configured via environment variables. See the `.env.example` file for the available options: -- `RUST_LOG`: Controls log verbosity (e.g., "info", "debug", "info,sqlx=warn", "geode_index=debug"). Defaults to "info,sqlx=warn,tracing_actix_web=error" when unset. +- `RUST_LOG`: Controls log verbosity (e.g., "info", "debug", "info,sqlx=warn", "geode_index=debug"). Defaults to "info,sqlx=warn,tracing_actix_web::middleware=error" when unset. - `LOG_FORMAT`: Output format - "text" (default, human-readable) or "json" (structured, for log aggregation). After all of this is done, you should be able to run `cargo run` inside the index directory. The migrations will be ran automatically, and the index will start. You can check `http://localhost:8080` (if you haven't changed the port in your .env file) to see if it all works. diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index d20001d6..08d50d03 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -71,9 +71,6 @@ impl actix_web::ResponseError for ApiError { } fn error_response(&self) -> HttpResponse { - if self.status_code().is_server_error() { - tracing::error!(error = ?self, "request failed"); - } HttpResponse::build(self.status_code()).json(self.as_response()) } } diff --git a/src/logging.rs b/src/logging.rs index b01615f9..7936ff56 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -3,14 +3,14 @@ use tracing_subscriber::EnvFilter; /// Initialize the global `tracing` subscriber. /// /// The filter is controlled by the `RUST_LOG` env var, falling back to -/// `"info,sqlx=warn,tracing_actix_web=error"` when unset. This default is -/// identical for debug and release builds. +/// `"info,sqlx=warn,tracing_actix_web::middleware=error"` when unset. This +/// default is identical for debug and release builds. /// /// The output format is controlled by the `LOG_FORMAT` env var: when it is /// exactly `"json"`, logs are emitted as JSON; otherwise the default /// compact/full text format is used. pub fn init() { - const DEFAULT_VALUE: &str = "info,sqlx=warn,tracing_actix_web=error"; + const DEFAULT_VALUE: &str = "info,sqlx=warn,tracing_actix_web::middleware=error"; let filter = EnvFilter::from(dotenvy::var("RUST_LOG").unwrap_or(DEFAULT_VALUE.into())); From e5d24b7d11956956dccfd9d37e09ea5b0cb2cd23 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 12 Jul 2026 16:14:48 +0300 Subject: [PATCH 67/69] feat: add back inspect_err to sqlx call sites --- src/database/repository/auth_tokens.rs | 12 +- src/database/repository/dependencies.rs | 2 + src/database/repository/deprecations.rs | 31 ++- src/database/repository/developers.rs | 26 +- .../repository/github_login_attempts.rs | 9 +- src/database/repository/github_web_logins.rs | 7 +- src/database/repository/incompatibilities.rs | 2 + src/database/repository/mod_downloads.rs | 7 +- src/database/repository/mod_gd_versions.rs | 4 +- src/database/repository/mod_links.rs | 3 +- src/database/repository/mod_tags.rs | 16 +- .../repository/mod_version_statuses.rs | 1 + .../repository/mod_version_submissions.rs | 29 ++- src/database/repository/mod_versions.rs | 28 ++- src/database/repository/mods.rs | 29 ++- src/database/repository/refresh_tokens.rs | 12 +- src/types/models/dependency.rs | 3 +- src/types/models/gd_version_alias.rs | 1 + src/types/models/incompatibility.rs | 6 +- src/types/models/loader_version.rs | 4 + src/types/models/mod_entity.rs | 25 +- src/types/models/mod_gd_version.rs | 6 +- src/types/models/mod_link.rs | 2 + src/types/models/mod_version.rs | 17 +- src/types/models/stats.rs | 226 +++++++++--------- src/types/models/tag.rs | 7 +- 26 files changed, 327 insertions(+), 188 deletions(-) diff --git a/src/database/repository/auth_tokens.rs b/src/database/repository/auth_tokens.rs index 345d80ce..01ade28b 100644 --- a/src/database/repository/auth_tokens.rs +++ b/src/database/repository/auth_tokens.rs @@ -28,7 +28,8 @@ pub async fn generate_token( expiry ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(token) } @@ -43,7 +44,8 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da hash ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -59,7 +61,8 @@ pub async fn remove_developer_tokens( developer_id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -71,7 +74,8 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { WHERE expires_at < NOW()" ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } diff --git a/src/database/repository/dependencies.rs b/src/database/repository/dependencies.rs index 957a55d3..0d71ce59 100644 --- a/src/database/repository/dependencies.rs +++ b/src/database/repository/dependencies.rs @@ -60,6 +60,7 @@ pub async fn create( ) .fetch_all(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -72,6 +73,7 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError ) .execute(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/deprecations.rs b/src/database/repository/deprecations.rs index a56055b4..8596513e 100644 --- a/src/database/repository/deprecations.rs +++ b/src/database/repository/deprecations.rs @@ -15,7 +15,8 @@ pub async fn get_for_mods( ids ) .fetch_all(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut bys: Vec<_> = sqlx::query!( "SELECT dby.deprecation_id, dby.by_mod_id @@ -24,7 +25,8 @@ pub async fn get_for_mods( &deps.iter().map(|d| d.id).collect::>() ) .fetch_all(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(deps .into_iter() @@ -52,7 +54,8 @@ pub async fn get(id: i32, conn: &mut PgConnection) -> Result id ) .fetch_optional(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .map(|x| Deprecation { id, mod_id: x.mod_id, @@ -73,7 +76,8 @@ pub async fn get(id: i32, conn: &mut PgConnection) -> Result dep.id ) .fetch_all(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; dep.by = deprecated_by.into_iter().map(|b| b.by_mod_id).collect(); @@ -97,7 +101,8 @@ pub async fn create( updated_by.id ) .fetch_one(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .id; if !by.is_empty() { @@ -138,7 +143,8 @@ pub async fn update( deprecation.id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; deprecation.reason = reason.to_string(); @@ -152,7 +158,8 @@ pub async fn update( deprecation.id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; insert_deprecated_by(deprecation.id, by, &mut *conn).await?; @@ -169,7 +176,8 @@ pub async fn update( deprecation.id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; } Ok(deprecation) @@ -183,7 +191,8 @@ pub async fn delete(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseErro id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -196,7 +205,8 @@ pub async fn clear_all(mod_id: &str, conn: &mut PgConnection) -> Result<(), Data mod_id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -225,6 +235,7 @@ async fn insert_deprecated_by( ) .execute(&mut *conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|_| ()) .map_err(|e| e.into()) } diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 38c4829f..70baaf81 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -37,7 +37,8 @@ pub async fn index( offset ) .fetch_all(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let count = index_count(query, &mut *conn).await?; @@ -63,6 +64,7 @@ pub async fn index_count( ) .fetch_one(&mut *conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|x| x.count.unwrap_or(0)) .map_err(|e| e.into()) } @@ -87,7 +89,8 @@ pub async fn fetch_or_insert_github( github_id ) .fetch_optional(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? { Some(dev) => Ok(dev), None => Ok(insert_github(github_id, username, conn).await?), @@ -116,6 +119,7 @@ async fn insert_github( ) .fetch_one(&mut *conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -136,6 +140,7 @@ pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result> = HashMap::new(); @@ -282,6 +291,7 @@ pub async fn has_access_to_mod( ) .fetch_optional(&mut *conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|x| x.is_some()) .map_err(|e| e.into()) } @@ -300,6 +310,7 @@ pub async fn has_active_mod(dev_id: i32, conn: &mut PgConnection) -> Result Result Result<(), Databas uuid ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -114,7 +118,8 @@ pub async fn poll_now(uuid: Uuid, conn: &mut PgConnection) -> Result<(), Databas pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_login_attempts WHERE uid = $1", uuid) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } diff --git a/src/database/repository/github_web_logins.rs b/src/database/repository/github_web_logins.rs index 7ddfd8e0..799e11b7 100644 --- a/src/database/repository/github_web_logins.rs +++ b/src/database/repository/github_web_logins.rs @@ -8,7 +8,8 @@ pub async fn create_unique(conn: &mut PgConnection) -> Result Result Result Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_web_logins WHERE state = $1", uuid) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } diff --git a/src/database/repository/incompatibilities.rs b/src/database/repository/incompatibilities.rs index 000a9126..85bd88f6 100644 --- a/src/database/repository/incompatibilities.rs +++ b/src/database/repository/incompatibilities.rs @@ -65,6 +65,7 @@ pub async fn create( ) .fetch_all(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -77,6 +78,7 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError ) .execute(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/mod_downloads.rs b/src/database/repository/mod_downloads.rs index 467b1435..4265ce13 100644 --- a/src/database/repository/mod_downloads.rs +++ b/src/database/repository/mod_downloads.rs @@ -17,7 +17,8 @@ pub async fn create( ip ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(result.rows_affected() > 0) } @@ -39,6 +40,7 @@ pub async fn has_downloaded_mod( ) .fetch_optional(&mut *conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|x| x.is_some()) } @@ -52,7 +54,8 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { date ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } diff --git a/src/database/repository/mod_gd_versions.rs b/src/database/repository/mod_gd_versions.rs index 2899bdcb..f694d21a 100644 --- a/src/database/repository/mod_gd_versions.rs +++ b/src/database/repository/mod_gd_versions.rs @@ -33,7 +33,8 @@ pub async fn create( &mod_id ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(json.gd.clone()) } @@ -47,6 +48,7 @@ pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), D ) .execute(&mut *conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|_| ()) } diff --git a/src/database/repository/mod_links.rs b/src/database/repository/mod_links.rs index a5dc8c3e..a224bd27 100644 --- a/src/database/repository/mod_links.rs +++ b/src/database/repository/mod_links.rs @@ -26,7 +26,8 @@ pub async fn upsert( source ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(ModLinks { mod_id: mod_id.into(), diff --git a/src/database/repository/mod_tags.rs b/src/database/repository/mod_tags.rs index f3d9ce48..66a7e9fb 100644 --- a/src/database/repository/mod_tags.rs +++ b/src/database/repository/mod_tags.rs @@ -15,7 +15,8 @@ pub async fn get_all_writable(conn: &mut PgConnection) -> Result, Datab where is_readonly = false" ) .fetch_all(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .into_iter() .map(|i| Tag { id: i.id, @@ -47,7 +48,8 @@ pub async fn get_allowed_for_mod( id ) .fetch_all(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .into_iter() .map(|i| Tag { id: i.id, @@ -73,7 +75,8 @@ pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> FROM mod_tags" ) .fetch_all(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .into_iter() .map(|i| Tag { id: i.id, @@ -101,6 +104,7 @@ pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, ) .fetch_all(&mut *conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|vec| { vec.into_iter() @@ -145,7 +149,8 @@ pub async fn update_for_mod( &deletable ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; } if !insertable.is_empty() { @@ -162,7 +167,8 @@ pub async fn update_for_mod( &insertable ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; } Ok(()) diff --git a/src/database/repository/mod_version_statuses.rs b/src/database/repository/mod_version_statuses.rs index 53eb4da1..e78e964b 100644 --- a/src/database/repository/mod_version_statuses.rs +++ b/src/database/repository/mod_version_statuses.rs @@ -20,6 +20,7 @@ pub async fn create( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|i| i.id) .map_err(|e| e.into()) } diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index fda47ecb..9f41821c 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -23,6 +23,7 @@ pub async fn get_for_mod_version( ) .fetch_optional(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -41,6 +42,7 @@ pub async fn get_audit_for_submission( ) .fetch_all(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -57,7 +59,8 @@ pub async fn create( mod_version_id ) .fetch_one(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; insert_submission_audit(mod_version_id, AuditAction::Created, None, None, conn).await?; Ok(row) @@ -103,6 +106,7 @@ pub async fn set_locked( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -128,6 +132,7 @@ pub async fn get_paginated_comments_for_submission( ) .fetch_all(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -142,6 +147,7 @@ pub async fn count_comments_for_submission( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } @@ -164,6 +170,7 @@ pub async fn create_comment( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -181,6 +188,7 @@ pub async fn get_comment( ) .fetch_optional(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -201,6 +209,7 @@ pub async fn update_comment( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -214,7 +223,8 @@ pub async fn delete_comment( comment_id ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(result.rows_affected() > 0) } @@ -233,6 +243,7 @@ pub async fn get_audit_for_comment( ) .fetch_all(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -247,6 +258,7 @@ pub async fn count_attachments_for_comment( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } @@ -267,6 +279,7 @@ pub async fn create_attachment( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -285,6 +298,7 @@ pub async fn get_attachments_for_comment( ) .fetch_all(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e: Error| e.into()) } @@ -302,7 +316,8 @@ pub async fn get_attachments_for_comments( comment_ids ) .fetch_all(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut ret: HashMap> = HashMap::with_capacity(comment_ids.len()); @@ -327,6 +342,7 @@ pub async fn get_attachment( ) .fetch_optional(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -340,7 +356,8 @@ pub async fn delete_attachment( attachment_id ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(result.rows_affected() > 0) } @@ -355,6 +372,7 @@ pub async fn count_references_to_filename( ) .fetch_one(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|c| c.unwrap_or(0)) .map_err(|e| e.into()) } @@ -374,6 +392,7 @@ pub async fn count_references_to_filenames( ) .fetch_all(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|x| { x.into_iter() .map(|record| (record.filename, record.count.unwrap_or(0))) @@ -400,6 +419,7 @@ pub async fn insert_submission_audit( ) .execute(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|_| ()) .map_err(|e| e.into()) } @@ -422,6 +442,7 @@ pub async fn insert_comment_audit( ) .execute(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|_| ()) .map_err(|e| e.into()) } diff --git a/src/database/repository/mod_versions.rs b/src/database/repository/mod_versions.rs index 045e16db..1abfb8b0 100644 --- a/src/database/repository/mod_versions.rs +++ b/src/database/repository/mod_versions.rs @@ -84,6 +84,7 @@ pub async fn get_by_version_str( ) .fetch_optional(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|opt| opt.map(|x| x.into_mod_version())) } @@ -112,7 +113,8 @@ pub async fn get_for_mod( statuses as Option<&[ModVersionStatusEnum]> ) .fetch_all(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let version_ids: Vec = records.iter().map(|x| x.id).collect(); let mut gd_versions = ModGDVersion::get_for_mod_versions(&version_ids, conn).await?; @@ -138,7 +140,8 @@ pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -151,7 +154,8 @@ pub async fn create_from_json( ) -> Result { sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey DEFERRED") .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let geode = Version::parse(&json.geode).or(Err(DatabaseError::InvalidInput( "mod.json geode version is invalid semver".into(), @@ -191,7 +195,8 @@ pub async fn create_from_json( json.requires_patching ) .fetch_one(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let id = row.id; @@ -207,11 +212,13 @@ pub async fn create_from_json( id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey IMMEDIATE") .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(ModVersion { id, @@ -302,7 +309,8 @@ pub async fn update_pending_version( version_id ) .fetch_one(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; if make_accepted { sqlx::query!( @@ -312,7 +320,8 @@ pub async fn update_pending_version( row.status_id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; } Ok(ModVersion { @@ -369,7 +378,8 @@ pub async fn update_version_status( version.id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; version.status = status; diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index 305dc0d1..9b550586 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -60,6 +60,7 @@ pub async fn get_one( ) .fetch_optional(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } else { @@ -74,6 +75,7 @@ pub async fn get_one( ) .fetch_optional(conn) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } @@ -104,6 +106,7 @@ pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result Result { Ok(sqlx::query!("SELECT featured FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .map(|row| row.featured) .unwrap_or(false)) } @@ -169,7 +175,8 @@ pub async fn is_featured(id: &str, conn: &mut PgConnection) -> Result Result { Ok(sqlx::query!("SELECT id FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .is_some()) } @@ -183,7 +190,8 @@ pub async fn exists_multiple( ) -> Result<(Vec, Vec), DatabaseError> { let mods: HashSet = sqlx::query!("SELECT id FROM mods WHERE id = ANY($1)", ids) .fetch_all(&mut *conn) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .into_iter() .map(|x| x.id) .collect(); @@ -219,7 +227,8 @@ pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result Result<() id ) .execute(&mut *conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -265,7 +275,8 @@ pub async fn update_with_json( the_mod.id ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; the_mod.repository = json.repository.clone(); the_mod.about = json.about.clone(); @@ -295,7 +306,8 @@ pub async fn update_with_json_moved( the_mod.id ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; the_mod.repository = json.repository; the_mod.about = json.about; @@ -315,7 +327,8 @@ pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), D id ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } diff --git a/src/database/repository/refresh_tokens.rs b/src/database/repository/refresh_tokens.rs index 75e0d16f..79b10fc7 100644 --- a/src/database/repository/refresh_tokens.rs +++ b/src/database/repository/refresh_tokens.rs @@ -20,7 +20,8 @@ pub async fn generate_token( expiry ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(token) } @@ -34,7 +35,8 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Da hash ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -50,7 +52,8 @@ pub async fn remove_developer_tokens( developer_id ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } @@ -62,7 +65,8 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { WHERE expires_at < NOW()" ) .execute(conn) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; Ok(()) } diff --git a/src/types/models/dependency.rs b/src/types/models/dependency.rs index 18952078..ae9d827c 100644 --- a/src/types/models/dependency.rs +++ b/src/types/models/dependency.rs @@ -191,7 +191,8 @@ impl Dependency { .bind(geode.map(|x| i32::try_from(x.patch).unwrap_or_default())) .bind(geode_pre) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut ret: HashMap> = HashMap::new(); for i in result { diff --git a/src/types/models/gd_version_alias.rs b/src/types/models/gd_version_alias.rs index 1d906594..2777b35f 100644 --- a/src/types/models/gd_version_alias.rs +++ b/src/types/models/gd_version_alias.rs @@ -86,6 +86,7 @@ impl GDVersionAlias { .build_query_scalar::() .fetch_optional(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } } diff --git a/src/types/models/incompatibility.rs b/src/types/models/incompatibility.rs index c57b8cf2..f196935b 100644 --- a/src/types/models/incompatibility.rs +++ b/src/types/models/incompatibility.rs @@ -107,6 +107,7 @@ impl Incompatibility { ) .fetch_all(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -162,7 +163,10 @@ impl Incompatibility { .bind(geode.map(|x| i64::try_from(x.patch).ok())) .bind(geode_pre); - let result = q.fetch_all(&mut *pool).await?; + let result = q + .fetch_all(&mut *pool) + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut ret: HashMap> = HashMap::new(); diff --git a/src/types/models/loader_version.rs b/src/types/models/loader_version.rs index 9f6862fb..0879cf98 100644 --- a/src/types/models/loader_version.rs +++ b/src/types/models/loader_version.rs @@ -165,6 +165,7 @@ impl LoaderVersion { .build_query_as::() .fetch_optional(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|x| x.map(|y| y.into_loader_version())) } @@ -185,6 +186,7 @@ impl LoaderVersion { ) .fetch_optional(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|x| x.map(|y| y.into_loader_version())) } @@ -209,6 +211,7 @@ impl LoaderVersion { ) .execute(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|_| ()) .map_err(|e| e.into()) } @@ -291,6 +294,7 @@ impl LoaderVersion { .build_query_as::() .fetch_all(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|x| x.into_iter().map(|y| y.into_loader_version()).collect()) .map_err(|e| e.into()) } diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index 1aeac382..4a4a5582 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -132,7 +132,8 @@ impl Mod { " ) .fetch_optional(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; if let Some((Some(total_count), Some(total_downloads))) = result.map(|o| (o.id_count, o.download_sum)) @@ -186,7 +187,7 @@ impl Mod { }; let order = match query.sort { - IndexSortType::Downloads => "q.download_count DESC", + IndexSortType::Downloads => "q.download_acount DESC", IndexSortType::RecentlyUpdated => "q.updated_at DESC", IndexSortType::RecentlyPublished => "q.created_at DESC", IndexSortType::Oldest => "q.created_at ASC", @@ -362,7 +363,8 @@ impl Mod { let records: Vec = records_builder .build_query_as() .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut count_builder = sqlx::QueryBuilder::new("SELECT COUNT(DISTINCT m.id) "); @@ -371,7 +373,8 @@ impl Mod { let count: i64 = count_builder .build_query_scalar() .fetch_optional(&mut *pool) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .unwrap_or_default(); if records.is_empty() { @@ -398,8 +401,8 @@ impl Mod { let mut developers = developers::get_all_for_mods(&ids, pool).await?; let links = ModLinks::fetch_for_mods(&ids, pool).await?; let mod_version_ids: Vec = versions - .iter() - .map(|(_, mod_version)| mod_version.id) + .values() + .map(|mod_version| mod_version.id) .collect(); let mut gd_versions = ModGDVersion::get_for_mod_versions(&mod_version_ids, pool).await?; @@ -528,7 +531,8 @@ impl Mod { only_owner ) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; if records.is_empty() { return Ok(vec![]); @@ -596,7 +600,8 @@ impl Mod { only_accepted ) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; if records.is_empty() { return Ok(None); @@ -678,6 +683,7 @@ impl Mod { sqlx::query!("UPDATE mods SET featured = $1 WHERE id = $2", featured, id) .execute(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|_| ()) } @@ -748,7 +754,8 @@ impl Mod { geode_pre ) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; if result.is_empty() { return Ok(vec![]); diff --git a/src/types/models/mod_gd_version.rs b/src/types/models/mod_gd_version.rs index fbd7bbd1..67d36e56 100644 --- a/src/types/models/mod_gd_version.rs +++ b/src/types/models/mod_gd_version.rs @@ -246,7 +246,8 @@ impl ModGDVersion { id ) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut ret = DetailedGDVersion { win: None, mac: None, @@ -298,7 +299,8 @@ impl ModGDVersion { versions ) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut ret: HashMap = HashMap::new(); for i in result { diff --git a/src/types/models/mod_link.rs b/src/types/models/mod_link.rs index e2d0751b..9fa7ea20 100644 --- a/src/types/models/mod_link.rs +++ b/src/types/models/mod_link.rs @@ -29,6 +29,7 @@ impl ModLinks { ) .fetch_optional(pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -51,6 +52,7 @@ impl ModLinks { ) .fetch_all(pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } } diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index d78290f5..9c5880bb 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -249,12 +249,14 @@ impl ModVersion { let records = q .build_query_as::() .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let count: i64 = counter_q .build_query_scalar() .fetch_one(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; if records.is_empty() { return Ok(PaginatedData { @@ -371,6 +373,7 @@ impl ModVersion { .bind(requires_patching) .fetch_all(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) .map(|result: Vec| { result.into_iter() @@ -402,7 +405,8 @@ impl ModVersion { ORDER BY mv.id DESC"#, ids ).fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut ret: HashMap> = HashMap::new(); @@ -471,7 +475,8 @@ impl ModVersion { let version = query_builder .build_query_as::() .fetch_optional(&mut *pool) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .map(|v| v.into_mod_version()); let Some(mut version) = version else { @@ -531,7 +536,8 @@ impl ModVersion { fetch_only_accepted ) .fetch_optional(&mut *pool) - .await? + .await + .inspect_err(|e| tracing::error!("{:?}", e))? .map(|x| x.into_mod_version()); let Some(mut version) = result else { @@ -576,6 +582,7 @@ impl ModVersion { ) .fetch_one(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|x| x.unwrap_or_default()) .map_err(|e| e.into()) } diff --git a/src/types/models/stats.rs b/src/types/models/stats.rs index 12e5110b..d99ed60a 100644 --- a/src/types/models/stats.rs +++ b/src/types/models/stats.rs @@ -1,112 +1,114 @@ -use crate::{database::repository::developers, endpoints::ApiError}; -use chrono::Utc; -use reqwest::{Client, header::HeaderValue}; -use serde::{Deserialize, Serialize}; -use sqlx::PgConnection; -use utoipa::ToSchema; - -use super::mod_entity::Mod; - -#[derive(Deserialize)] -struct GithubReleaseAsset { - // Github says in its API specs it's an integer, so theoretically it might - // return a download count of -1 or something - download_count: i64, -} - -#[derive(Deserialize)] -struct GithubReleaseWithAssets { - tag_name: String, - assets: Vec, -} - -#[derive(Deserialize, Serialize, Clone, Debug, ToSchema)] -pub struct Stats { - pub total_geode_downloads: i64, - pub total_mod_count: i64, - pub total_mod_downloads: i64, - pub total_registered_developers: i64, -} - -impl Stats { - #[tracing::instrument(skip_all)] - pub async fn get_cached(pool: &mut PgConnection) -> Result { - let mod_stats = Mod::get_stats(&mut *pool).await?; - Ok(Stats { - total_mod_count: mod_stats.total_count, - total_mod_downloads: mod_stats.total_downloads, - total_registered_developers: developers::index_count(None, &mut *pool).await?, - total_geode_downloads: Self::get_latest_github_release_download_count(&mut *pool) - .await?, - }) - } - - #[tracing::instrument(skip_all)] - async fn get_latest_github_release_download_count( - pool: &mut PgConnection, - ) -> Result { - // If release stats were fetched less than a day ago, just use cached stats - if let Ok((cache_time, total_download_count)) = sqlx::query!( - "SELECT s.checked_at, s.total_download_count - FROM github_loader_release_stats s - ORDER BY s.checked_at DESC" - ) - .fetch_one(&mut *pool) - .await - .map(|d| (d.checked_at, d.total_download_count)) - { - if Utc::now().signed_duration_since(cache_time).num_days() < 1 { - return Ok(total_download_count); - } - } - - // Fetch latest stats - let new = Self::fetch_github_release_stats().await?; - sqlx::query!( - "INSERT INTO github_loader_release_stats (total_download_count, latest_loader_version) - VALUES ($1, $2)", - new.0, - new.1 - ) - .execute(&mut *pool) - .await?; - Ok(new.0) - } - - async fn fetch_github_release_stats() -> Result<(i64, String), ApiError> { - let client = Client::new(); - let resp = client - .get("https://api.github.com/repos/geode-sdk/geode/releases") - .header("Accept", HeaderValue::from_str("application/json").unwrap()) - .header("User-Agent", "geode_index") - .query(&[("per_page", "100")]) - .send() - .await - .inspect_err(|e| { - tracing::error!("Failed to request Geode release stats from GitHub: {}", e) - })?; - - if !resp.status().is_success() { - return Err(ApiError::InternalError( - "Failed to retrieve Geode release stats from GitHub".into(), - )); - } - - let releases: Vec = resp.json().await?; - let latest_release_tag = releases - .iter() - .find(|r| r.tag_name != "nightly") - .ok_or(ApiError::InternalError( - "No latest version detected on Geode repository".into(), - ))? - .tag_name - .clone(); - Ok(( - releases - .into_iter() - .map(|r| r.assets.into_iter().map(|a| a.download_count).sum::()) - .sum(), - latest_release_tag, - )) - } -} +use crate::{database::repository::developers, endpoints::ApiError}; +use chrono::Utc; +use reqwest::{Client, header::HeaderValue}; +use serde::{Deserialize, Serialize}; +use sqlx::PgConnection; +use utoipa::ToSchema; + +use super::mod_entity::Mod; + +#[derive(Deserialize)] +struct GithubReleaseAsset { + // Github says in its API specs it's an integer, so theoretically it might + // return a download count of -1 or something + download_count: i64, +} + +#[derive(Deserialize)] +struct GithubReleaseWithAssets { + tag_name: String, + assets: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, ToSchema)] +pub struct Stats { + pub total_geode_downloads: i64, + pub total_mod_count: i64, + pub total_mod_downloads: i64, + pub total_registered_developers: i64, +} + +impl Stats { + #[tracing::instrument(skip_all)] + pub async fn get_cached(pool: &mut PgConnection) -> Result { + let mod_stats = Mod::get_stats(&mut *pool).await?; + Ok(Stats { + total_mod_count: mod_stats.total_count, + total_mod_downloads: mod_stats.total_downloads, + total_registered_developers: developers::index_count(None, &mut *pool).await?, + total_geode_downloads: Self::get_latest_github_release_download_count(&mut *pool) + .await?, + }) + } + + #[tracing::instrument(skip_all)] + async fn get_latest_github_release_download_count( + pool: &mut PgConnection, + ) -> Result { + // If release stats were fetched less than a day ago, just use cached stats + if let Ok((cache_time, total_download_count)) = sqlx::query!( + "SELECT s.checked_at, s.total_download_count + FROM github_loader_release_stats s + ORDER BY s.checked_at DESC" + ) + .fetch_one(&mut *pool) + .await + .inspect_err(|e| tracing::error!("{:?}", e)) + .map(|d| (d.checked_at, d.total_download_count)) + { + if Utc::now().signed_duration_since(cache_time).num_days() < 1 { + return Ok(total_download_count); + } + } + + // Fetch latest stats + let new = Self::fetch_github_release_stats().await?; + sqlx::query!( + "INSERT INTO github_loader_release_stats (total_download_count, latest_loader_version) + VALUES ($1, $2)", + new.0, + new.1 + ) + .execute(&mut *pool) + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; + Ok(new.0) + } + + async fn fetch_github_release_stats() -> Result<(i64, String), ApiError> { + let client = Client::new(); + let resp = client + .get("https://api.github.com/repos/geode-sdk/geode/releases") + .header("Accept", HeaderValue::from_str("application/json").unwrap()) + .header("User-Agent", "geode_index") + .query(&[("per_page", "100")]) + .send() + .await + .inspect_err(|e| { + tracing::error!("Failed to request Geode release stats from GitHub: {}", e) + })?; + + if !resp.status().is_success() { + return Err(ApiError::InternalError( + "Failed to retrieve Geode release stats from GitHub".into(), + )); + } + + let releases: Vec = resp.json().await?; + let latest_release_tag = releases + .iter() + .find(|r| r.tag_name != "nightly") + .ok_or(ApiError::InternalError( + "No latest version detected on Geode repository".into(), + ))? + .tag_name + .clone(); + Ok(( + releases + .into_iter() + .map(|r| r.assets.into_iter().map(|a| a.download_count).sum::()) + .sum(), + latest_release_tag, + )) + } +} diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index 3f6f3740..0d6f1382 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -27,6 +27,7 @@ impl Tag { ) .fetch_all(&mut *pool) .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map(|tags| tags.into_iter().map(|t| t.name).collect::>()) .map_err(|e| e.into()) } @@ -43,7 +44,8 @@ impl Tag { ids ) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let mut ret: HashMap> = HashMap::new(); for tag in tags { @@ -69,7 +71,8 @@ impl Tag { &tags ) .fetch_all(&mut *pool) - .await?; + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; let fetched_ids = fetched.iter().map(|t| t.id).collect::>(); let fetched_names = fetched From 7614809259e0ae332307faa47b2363f33699f7d1 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 12 Jul 2026 16:16:27 +0300 Subject: [PATCH 68/69] chore: run cargo fmt --- src/auth/github.rs | 12 +- src/config.rs | 3 +- src/database/mod.rs | 2 +- .../repository/github_login_attempts.rs | 2 +- src/database/repository/mod_downloads.rs | 2 +- src/database/repository/mod_tags.rs | 2 +- .../repository/mod_version_submissions.rs | 3 +- src/database/repository/mod_versions.rs | 3 +- src/endpoints/auth/github.rs | 19 +- src/endpoints/auth/mod.rs | 4 +- src/endpoints/deprecations.rs | 602 +++++++++--------- src/endpoints/developers.rs | 14 +- src/endpoints/loader.rs | 4 +- src/endpoints/mod.rs | 8 +- src/endpoints/mod_status_badge.rs | 17 +- src/endpoints/mod_version_submissions.rs | 38 +- src/endpoints/mod_versions.rs | 3 +- src/endpoints/mods.rs | 25 +- src/endpoints/stats.rs | 48 +- src/endpoints/tags.rs | 2 +- src/events/mod.rs | 2 +- src/extractors/auth.rs | 2 +- src/jobs/cleanup_downloads.rs | 1 - src/jobs/migrate.rs | 2 +- src/logging.rs | 4 +- src/main.rs | 7 +- src/storage.rs | 8 +- src/types/mod_json.rs | 14 +- src/types/models/audit_actions.rs | 2 +- src/types/models/dependency.rs | 6 +- src/types/models/developer.rs | 4 +- src/types/models/gd_version_alias.rs | 2 +- src/types/models/mod_version.rs | 4 +- src/types/models/mod_version_submission.rs | 7 +- src/types/models/tag.rs | 2 +- src/types/serde/mod.rs | 2 +- src/webhook/discord.rs | 14 +- src/webhook/mod.rs | 2 +- 38 files changed, 453 insertions(+), 445 deletions(-) diff --git a/src/auth/github.rs b/src/auth/github.rs index 52a1838c..33366f0e 100644 --- a/src/auth/github.rs +++ b/src/auth/github.rs @@ -1,10 +1,10 @@ use crate::auth::AuthenticationError; use crate::database::repository::github_login_attempts; use crate::types::models::github_login_attempt::StoredLoginAttempt; -use reqwest::{header::HeaderValue, Client}; +use reqwest::{Client, header::HeaderValue}; use serde::{Deserialize, Serialize}; use serde_json::json; -use sqlx::{types::ipnetwork::IpNetwork, PgConnection}; +use sqlx::{PgConnection, types::ipnetwork::IpNetwork}; use uuid::Uuid; #[derive(Debug, Deserialize, Serialize)] @@ -108,7 +108,9 @@ impl GithubClient { })) .send() .await - .inspect_err(|e| tracing::error!("Failed to start OAuth device flow with GitHub: {e}"))?; + .inspect_err(|e| { + tracing::error!("Failed to start OAuth device flow with GitHub: {e}") + })?; if !res.status().is_success() { tracing::error!( @@ -259,7 +261,9 @@ impl GithubClient { let body = resp .json::() .await - .inspect_err(|e| tracing::error!("github::get_installation: failed to parse response: {e}")) + .inspect_err(|e| { + tracing::error!("github::get_installation: failed to parse response: {e}") + }) .or(Err(AuthenticationError::InternalError( "Failed to parse response from GitHub".into(), )))?; diff --git a/src/config.rs b/src/config.rs index 477357af..cfda4ee1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,7 +54,8 @@ pub async fn build_config() -> anyhow::Result { let github_client = dotenvy::var("GITHUB_CLIENT_ID").unwrap_or("".to_string()); let github_secret = dotenvy::var("GITHUB_CLIENT_SECRET").unwrap_or("".to_string()); let webhook_url = dotenvy::var("DISCORD_WEBHOOK_URL").unwrap_or("".to_string()); - let index_admin_webhook_url = dotenvy::var("INDEX_ADMIN_DISCORD_WEBHOOK_URL").unwrap_or("".to_string()); + let index_admin_webhook_url = + dotenvy::var("INDEX_ADMIN_DISCORD_WEBHOOK_URL").unwrap_or("".to_string()); let disable_downloads = dotenvy::var("DISABLE_DOWNLOAD_COUNTS").unwrap_or("0".to_string()) == "1"; let max_download_mb = dotenvy::var("MAX_MOD_FILESIZE_MB") diff --git a/src/database/mod.rs b/src/database/mod.rs index 294e31fd..b19a744b 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -5,5 +5,5 @@ pub enum DatabaseError { #[error("Invalid input: {0}")] InvalidInput(String), #[error("Unknown database error")] - SqlxError(#[from] sqlx::Error) + SqlxError(#[from] sqlx::Error), } diff --git a/src/database/repository/github_login_attempts.rs b/src/database/repository/github_login_attempts.rs index 4a76a6e0..0dcbfa6f 100644 --- a/src/database/repository/github_login_attempts.rs +++ b/src/database/repository/github_login_attempts.rs @@ -1,8 +1,8 @@ use crate::database::DatabaseError; use crate::types::models::github_login_attempt::StoredLoginAttempt; use chrono::Utc; -use sqlx::types::ipnetwork::IpNetwork; use sqlx::PgConnection; +use sqlx::types::ipnetwork::IpNetwork; use uuid::Uuid; #[tracing::instrument(skip_all)] diff --git a/src/database/repository/mod_downloads.rs b/src/database/repository/mod_downloads.rs index 4265ce13..3decb4d9 100644 --- a/src/database/repository/mod_downloads.rs +++ b/src/database/repository/mod_downloads.rs @@ -1,7 +1,7 @@ use crate::database::DatabaseError; use chrono::{Days, Utc}; -use sqlx::types::ipnetwork::IpNetwork; use sqlx::PgConnection; +use sqlx::types::ipnetwork::IpNetwork; #[tracing::instrument(skip_all, fields(mod_version_id = %mod_version_id))] pub async fn create( diff --git a/src/database/repository/mod_tags.rs b/src/database/repository/mod_tags.rs index 66a7e9fb..2d15efc4 100644 --- a/src/database/repository/mod_tags.rs +++ b/src/database/repository/mod_tags.rs @@ -1,7 +1,7 @@ -use std::collections::HashSet; use crate::database::DatabaseError; use crate::types::models::tag::Tag; use sqlx::PgConnection; +use std::collections::HashSet; #[tracing::instrument(skip_all)] pub async fn get_all_writable(conn: &mut PgConnection) -> Result, DatabaseError> { diff --git a/src/database/repository/mod_version_submissions.rs b/src/database/repository/mod_version_submissions.rs index 9f41821c..caf4425c 100644 --- a/src/database/repository/mod_version_submissions.rs +++ b/src/database/repository/mod_version_submissions.rs @@ -319,7 +319,8 @@ pub async fn get_attachments_for_comments( .await .inspect_err(|e| tracing::error!("{:?}", e))?; - let mut ret: HashMap> = HashMap::with_capacity(comment_ids.len()); + let mut ret: HashMap> = + HashMap::with_capacity(comment_ids.len()); for row in rows { ret.entry(row.comment_id).or_default().push(row); diff --git a/src/database/repository/mod_versions.rs b/src/database/repository/mod_versions.rs index 1abfb8b0..98a99e41 100644 --- a/src/database/repository/mod_versions.rs +++ b/src/database/repository/mod_versions.rs @@ -3,7 +3,8 @@ use crate::database::DatabaseError; use crate::types::{ mod_json::ModJson, models::{ - developer::Developer, mod_version::ModVersion, mod_version_status::ModVersionStatusEnum, mod_gd_version::ModGDVersion, + developer::Developer, mod_gd_version::ModGDVersion, mod_version::ModVersion, + mod_version_status::ModVersionStatusEnum, }, }; use chrono::{DateTime, Utc}; diff --git a/src/endpoints/auth/github.rs b/src/endpoints/auth/github.rs index 14177a79..d79b988c 100644 --- a/src/endpoints/auth/github.rs +++ b/src/endpoints/auth/github.rs @@ -1,16 +1,16 @@ use actix_web::http::StatusCode; -use actix_web::{dev::ConnectionInfo, post, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, dev::ConnectionInfo, post, web}; use serde::Deserialize; -use sqlx::{types::ipnetwork::IpNetwork, Acquire}; -use uuid::Uuid; +use sqlx::{Acquire, types::ipnetwork::IpNetwork}; use utoipa::ToSchema; +use uuid::Uuid; use crate::config::AppData; use crate::database::repository::{ auth_tokens, developers, github_login_attempts, github_web_logins, refresh_tokens, }; -use crate::endpoints::auth::TokensResponse; use crate::endpoints::ApiError; +use crate::endpoints::auth::TokensResponse; use crate::{auth::github, types::api::ApiResponse}; #[derive(Deserialize, ToSchema)] @@ -282,13 +282,10 @@ pub async fn github_token_login( ); let user = match client.get_user(&json.token).await { - Err(_) => client - .get_installation(&json.token) - .await - .map_err(|e| { - tracing::error!(error = ?e, "invalid access token"); - ApiError::BadRequest(format!("Invalid access token: {}", json.token)) - })?, + Err(_) => client.get_installation(&json.token).await.map_err(|e| { + tracing::error!(error = ?e, "invalid access token"); + ApiError::BadRequest(format!("Invalid access token: {}", json.token)) + })?, Ok(u) => u, }; diff --git a/src/endpoints/auth/mod.rs b/src/endpoints/auth/mod.rs index c089a796..286a08c2 100644 --- a/src/endpoints/auth/mod.rs +++ b/src/endpoints/auth/mod.rs @@ -3,11 +3,11 @@ use crate::database::repository::{auth_tokens, developers, refresh_tokens}; use crate::endpoints::ApiError; use crate::extractors::auth::Auth; use crate::types::api::ApiResponse; -use actix_web::{post, web, Responder}; +use actix_web::{Responder, post, web}; use serde::{Deserialize, Serialize}; use sqlx::Acquire; -use uuid::Uuid; use utoipa::ToSchema; +use uuid::Uuid; pub mod github; diff --git a/src/endpoints/deprecations.rs b/src/endpoints/deprecations.rs index 826e38be..1d06cab6 100644 --- a/src/endpoints/deprecations.rs +++ b/src/endpoints/deprecations.rs @@ -1,301 +1,301 @@ -use crate::{ - config::AppData, - database::repository::{deprecations, developers, mods}, - endpoints::ApiError, - extractors::auth::Auth, - types::api::ApiResponse, - types::models::deprecations::Deprecation, -}; -use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; -use serde::Deserialize; -use sqlx::{Acquire, PgConnection}; -use utoipa::{IntoParams, ToSchema}; - -#[derive(Deserialize, IntoParams)] -struct ModPath { - id: String, -} - -#[derive(Deserialize, IntoParams)] -struct ModDeprecationPath { - id: String, - deprecation_id: i32, -} - -#[derive(Deserialize, ToSchema)] -struct CreateDeprecationData { - by: Vec, - reason: String, -} - -#[derive(Deserialize, ToSchema)] -struct UpdateDeprecationData { - by: Option>, - reason: Option, -} - -const MAX_MODS_PER_DEPRECATION: usize = 20; - -/// Fetch all deprecations for a mod -#[utoipa::path( - get, - path = "/v1/mods/{id}/deprecations", - tag = "deprecations", - params(ModPath), - responses( - (status = 200, description = "List of deprecations", body = inline(ApiResponse>)), - (status = 404, description = "Mod not found") - ) -)] -#[get("v1/mods/{id}/deprecations")] -#[tracing::instrument(skip_all, fields(mod_id = %path.id))] -pub async fn index( - data: web::Data, - path: web::Path -) -> Result { - let mut pool = data.db().acquire().await?; - - if !mods::exists(&path.id, &mut pool).await? { - return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); - } - - let deps = deprecations::get_for_mods(std::slice::from_ref(&path.id), &mut pool).await?; - Ok(web::Json(ApiResponse { - error: "".into(), - payload: deps, - })) -} - -/// Insert one deprecation for a mod -#[utoipa::path( - post, - path = "/v1/mods/{id}/deprecations", - tag = "deprecations", - params(ModPath), - request_body = CreateDeprecationData, - responses( - (status = 201, description = "Deprecation created", body = inline(ApiResponse)), - (status = 400, description = "Bad request"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Mod not found") - ), - security( - ("bearer_token" = []) - ) -)] -#[post("v1/mods/{id}/deprecations")] -#[tracing::instrument(skip_all, fields(mod_id = %path.id))] -pub async fn store( - data: web::Data, - path: web::Path, - json: web::Json, - auth: Auth, -) -> Result { - let dev = auth.developer()?; - let mut pool = data.db().acquire().await?; - let mut tx = pool.begin().await?; - - if !mods::exists(&path.id, &mut tx).await? { - return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); - } - - if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut tx).await? { - return Err(ApiError::Authorization); - } - - check_existing_mods(&json.by, &mut tx).await?; - - let deprecation = deprecations::create(&path.id, &json.by, &json.reason, &dev, &mut tx).await?; - - tx.commit().await?; - Ok(HttpResponse::Created().json(ApiResponse { - error: "".into(), - payload: deprecation, - })) -} - -/// Update a deprecation -#[utoipa::path( - put, - path = "/v1/mods/{id}/deprecations/{deprecation_id}", - tag = "deprecations", - params(ModDeprecationPath), - request_body = UpdateDeprecationData, - responses( - (status = 200, description = "Deprecation updated", body = inline(ApiResponse)), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Deprecation not found") - ), - security( - ("bearer_token" = []) - ) -)] -#[put("v1/mods/{id}/deprecations/{deprecation_id}")] -#[tracing::instrument(skip_all, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] -pub async fn update( - data: web::Data, - path: web::Path, - json: web::Json, - auth: Auth, -) -> Result { - let dev = auth.developer()?; - let mut pool = data.db().acquire().await?; - let mut tx = pool.begin().await?; - - if !mods::exists(&path.id, &mut tx).await? { - return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); - } - - if let Some(by) = &json.by { - check_existing_mods(by, &mut tx).await?; - } - - let deprecation = deprecations::get(path.deprecation_id, &mut tx) - .await? - .ok_or(ApiError::NotFound(format!( - "Deprecation id {} not found", - path.deprecation_id - )))?; - - // If the ID doesn't match, just ignore it - if deprecation.mod_id != path.id { - return Err(ApiError::NotFound(format!( - "Deprecation id {} not found", - path.deprecation_id - ))); - } - - if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut tx).await? { - return Err(ApiError::Authorization); - } - - let updated = deprecations::update( - deprecation, - json.by.as_deref(), - json.reason.as_deref(), - &dev, - &mut tx, - ) - .await?; - - tx.commit().await?; - Ok(HttpResponse::Ok().json(ApiResponse { - error: "".into(), - payload: updated, - })) -} - -/// Delete a deprecation -#[utoipa::path( - delete, - path = "/v1/mods/{id}/deprecations/{deprecation_id}", - tag = "deprecations", - params(ModDeprecationPath), - responses( - (status = 204, description = "Deprecation deleted"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Deprecation not found") - ), - security( - ("bearer_token" = []) - ) -)] -#[delete("v1/mods/{id}/deprecations/{deprecation_id}")] -#[tracing::instrument(skip_all, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] -pub async fn delete( - data: web::Data, - path: web::Path, - auth: Auth, -) -> Result { - let dev = auth.developer()?; - let mut pool = data.db().acquire().await?; - - if !mods::exists(&path.id, &mut pool).await? { - return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); - } - - let deprecation = deprecations::get(path.deprecation_id, &mut pool) - .await? - .ok_or(ApiError::NotFound(format!( - "Deprecation id {} not found", - path.deprecation_id - )))?; - - // If the ID doesn't match, just ignore it - if deprecation.mod_id != path.id { - return Err(ApiError::NotFound(format!( - "Deprecation id {} not found", - path.deprecation_id - ))); - } - - if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut pool).await? { - return Err(ApiError::Authorization); - } - - deprecations::delete(deprecation.id, &mut pool).await?; - - Ok(HttpResponse::NoContent()) -} - -/// Delete all deprecations for a mod -#[utoipa::path( - delete, - path = "/v1/mods/{id}/deprecations", - tag = "deprecations", - params(ModPath), - responses( - (status = 204, description = "All deprecations deleted"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Mod not found") - ), - security( - ("bearer_token" = []) - ) -)] -#[delete("v1/mods/{id}/deprecations")] -#[tracing::instrument(skip_all, fields(mod_id = %path.id))] -pub async fn clear_all( - data: web::Data, - path: web::Path, - auth: Auth, -) -> Result { - let dev = auth.developer()?; - let mut pool = data.db().acquire().await?; - - if !mods::exists(&path.id, &mut pool).await? { - return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); - } - - if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut pool).await? { - return Err(ApiError::Authorization); - } - - deprecations::clear_all(&path.id, &mut pool).await?; - - Ok(HttpResponse::NoContent()) -} - -async fn check_existing_mods(ids: &[String], conn: &mut PgConnection) -> Result<(), ApiError> { - if ids.len() > MAX_MODS_PER_DEPRECATION { - return Err(ApiError::BadRequest(format!( - "Max {} mods allowed per deprecation", - MAX_MODS_PER_DEPRECATION - ))); - } - - let (_, missing) = mods::exists_multiple(ids, &mut *conn).await?; - - if !missing.is_empty() { - return Err(ApiError::BadRequest(format!( - "The following mods don't exist on the index: {}", - missing.join(", ") - ))); - } - - Ok(()) -} +use crate::{ + config::AppData, + database::repository::{deprecations, developers, mods}, + endpoints::ApiError, + extractors::auth::Auth, + types::api::ApiResponse, + types::models::deprecations::Deprecation, +}; +use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; +use serde::Deserialize; +use sqlx::{Acquire, PgConnection}; +use utoipa::{IntoParams, ToSchema}; + +#[derive(Deserialize, IntoParams)] +struct ModPath { + id: String, +} + +#[derive(Deserialize, IntoParams)] +struct ModDeprecationPath { + id: String, + deprecation_id: i32, +} + +#[derive(Deserialize, ToSchema)] +struct CreateDeprecationData { + by: Vec, + reason: String, +} + +#[derive(Deserialize, ToSchema)] +struct UpdateDeprecationData { + by: Option>, + reason: Option, +} + +const MAX_MODS_PER_DEPRECATION: usize = 20; + +/// Fetch all deprecations for a mod +#[utoipa::path( + get, + path = "/v1/mods/{id}/deprecations", + tag = "deprecations", + params(ModPath), + responses( + (status = 200, description = "List of deprecations", body = inline(ApiResponse>)), + (status = 404, description = "Mod not found") + ) +)] +#[get("v1/mods/{id}/deprecations")] +#[tracing::instrument(skip_all, fields(mod_id = %path.id))] +pub async fn index( + data: web::Data, + path: web::Path, +) -> Result { + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); + } + + let deps = deprecations::get_for_mods(std::slice::from_ref(&path.id), &mut pool).await?; + Ok(web::Json(ApiResponse { + error: "".into(), + payload: deps, + })) +} + +/// Insert one deprecation for a mod +#[utoipa::path( + post, + path = "/v1/mods/{id}/deprecations", + tag = "deprecations", + params(ModPath), + request_body = CreateDeprecationData, + responses( + (status = 201, description = "Deprecation created", body = inline(ApiResponse)), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Mod not found") + ), + security( + ("bearer_token" = []) + ) +)] +#[post("v1/mods/{id}/deprecations")] +#[tracing::instrument(skip_all, fields(mod_id = %path.id))] +pub async fn store( + data: web::Data, + path: web::Path, + json: web::Json, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db().acquire().await?; + let mut tx = pool.begin().await?; + + if !mods::exists(&path.id, &mut tx).await? { + return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); + } + + if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut tx).await? { + return Err(ApiError::Authorization); + } + + check_existing_mods(&json.by, &mut tx).await?; + + let deprecation = deprecations::create(&path.id, &json.by, &json.reason, &dev, &mut tx).await?; + + tx.commit().await?; + Ok(HttpResponse::Created().json(ApiResponse { + error: "".into(), + payload: deprecation, + })) +} + +/// Update a deprecation +#[utoipa::path( + put, + path = "/v1/mods/{id}/deprecations/{deprecation_id}", + tag = "deprecations", + params(ModDeprecationPath), + request_body = UpdateDeprecationData, + responses( + (status = 200, description = "Deprecation updated", body = inline(ApiResponse)), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Deprecation not found") + ), + security( + ("bearer_token" = []) + ) +)] +#[put("v1/mods/{id}/deprecations/{deprecation_id}")] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] +pub async fn update( + data: web::Data, + path: web::Path, + json: web::Json, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db().acquire().await?; + let mut tx = pool.begin().await?; + + if !mods::exists(&path.id, &mut tx).await? { + return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); + } + + if let Some(by) = &json.by { + check_existing_mods(by, &mut tx).await?; + } + + let deprecation = deprecations::get(path.deprecation_id, &mut tx) + .await? + .ok_or(ApiError::NotFound(format!( + "Deprecation id {} not found", + path.deprecation_id + )))?; + + // If the ID doesn't match, just ignore it + if deprecation.mod_id != path.id { + return Err(ApiError::NotFound(format!( + "Deprecation id {} not found", + path.deprecation_id + ))); + } + + if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut tx).await? { + return Err(ApiError::Authorization); + } + + let updated = deprecations::update( + deprecation, + json.by.as_deref(), + json.reason.as_deref(), + &dev, + &mut tx, + ) + .await?; + + tx.commit().await?; + Ok(HttpResponse::Ok().json(ApiResponse { + error: "".into(), + payload: updated, + })) +} + +/// Delete a deprecation +#[utoipa::path( + delete, + path = "/v1/mods/{id}/deprecations/{deprecation_id}", + tag = "deprecations", + params(ModDeprecationPath), + responses( + (status = 204, description = "Deprecation deleted"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Deprecation not found") + ), + security( + ("bearer_token" = []) + ) +)] +#[delete("v1/mods/{id}/deprecations/{deprecation_id}")] +#[tracing::instrument(skip_all, fields(mod_id = %path.id, deprecation_id = %path.deprecation_id))] +pub async fn delete( + data: web::Data, + path: web::Path, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); + } + + let deprecation = deprecations::get(path.deprecation_id, &mut pool) + .await? + .ok_or(ApiError::NotFound(format!( + "Deprecation id {} not found", + path.deprecation_id + )))?; + + // If the ID doesn't match, just ignore it + if deprecation.mod_id != path.id { + return Err(ApiError::NotFound(format!( + "Deprecation id {} not found", + path.deprecation_id + ))); + } + + if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut pool).await? { + return Err(ApiError::Authorization); + } + + deprecations::delete(deprecation.id, &mut pool).await?; + + Ok(HttpResponse::NoContent()) +} + +/// Delete all deprecations for a mod +#[utoipa::path( + delete, + path = "/v1/mods/{id}/deprecations", + tag = "deprecations", + params(ModPath), + responses( + (status = 204, description = "All deprecations deleted"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Mod not found") + ), + security( + ("bearer_token" = []) + ) +)] +#[delete("v1/mods/{id}/deprecations")] +#[tracing::instrument(skip_all, fields(mod_id = %path.id))] +pub async fn clear_all( + data: web::Data, + path: web::Path, + auth: Auth, +) -> Result { + let dev = auth.developer()?; + let mut pool = data.db().acquire().await?; + + if !mods::exists(&path.id, &mut pool).await? { + return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); + } + + if !dev.admin && !developers::owns_mod(dev.id, &path.id, &mut pool).await? { + return Err(ApiError::Authorization); + } + + deprecations::clear_all(&path.id, &mut pool).await?; + + Ok(HttpResponse::NoContent()) +} + +async fn check_existing_mods(ids: &[String], conn: &mut PgConnection) -> Result<(), ApiError> { + if ids.len() > MAX_MODS_PER_DEPRECATION { + return Err(ApiError::BadRequest(format!( + "Max {} mods allowed per deprecation", + MAX_MODS_PER_DEPRECATION + ))); + } + + let (_, missing) = mods::exists_multiple(ids, &mut *conn).await?; + + if !missing.is_empty() { + return Err(ApiError::BadRequest(format!( + "The following mods don't exist on the index: {}", + missing.join(", ") + ))); + } + + Ok(()) +} diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index 4acdb0c9..670fc8bc 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -1,6 +1,6 @@ -use actix_web::{delete, get, post, put, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use serde::{Deserialize, Serialize}; -use utoipa::{ToSchema, IntoParams}; +use utoipa::{IntoParams, ToSchema}; use super::ApiError; use crate::config::AppData; @@ -9,12 +9,10 @@ use crate::types::api::{ApiResponse, PaginatedData}; use crate::types::models::developer::SelfDeveloper; use crate::{ extractors::auth::Auth, - types::{ - models::{ - developer::{ModDeveloper, Developer}, - mod_entity::Mod, - mod_version_status::ModVersionStatusEnum, - }, + types::models::{ + developer::{Developer, ModDeveloper}, + mod_entity::Mod, + mod_version_status::ModVersionStatusEnum, }, }; diff --git a/src/endpoints/loader.rs b/src/endpoints/loader.rs index eb5df2cc..34345668 100644 --- a/src/endpoints/loader.rs +++ b/src/endpoints/loader.rs @@ -1,7 +1,7 @@ -use actix_web::{get, post, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, get, post, web}; use serde::Deserialize; use std::str::FromStr; -use utoipa::{ToSchema, IntoParams}; +use utoipa::{IntoParams, ToSchema}; use sqlx::Acquire; diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 08d50d03..581fd26a 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -2,19 +2,19 @@ use crate::{ mod_zip::ModZipError, types::{api::ApiResponse, models::mod_gd_version::PlatformParseError}, }; -use actix_web::{http::StatusCode, HttpResponse}; +use actix_web::{HttpResponse, http::StatusCode}; pub mod auth; +pub mod deprecations; pub mod developers; pub mod health; pub mod loader; -pub mod mod_versions; pub mod mod_status_badge; +pub mod mod_version_submissions; +pub mod mod_versions; pub mod mods; pub mod stats; pub mod tags; -pub mod deprecations; -pub mod mod_version_submissions; #[derive(thiserror::Error, Debug)] pub enum ApiError { diff --git a/src/endpoints/mod_status_badge.rs b/src/endpoints/mod_status_badge.rs index 75e8c478..75f1778b 100644 --- a/src/endpoints/mod_status_badge.rs +++ b/src/endpoints/mod_status_badge.rs @@ -67,17 +67,16 @@ pub async fn status_badge( "shields/mod_downloads.svg", ), }; - let svg = data - .static_storage() - .read(svg_path) - .await - .map_err(|e| { - tracing::error!(error = ?e, "failed to read status badge file"); - ApiError::InternalError("Failed to read status badge file".into()) - })?; + let svg = data.static_storage().read(svg_path).await.map_err(|e| { + tracing::error!(error = ?e, "failed to read status badge file"); + ApiError::InternalError("Failed to read status badge file".into()) + })?; let api_url = format!("{}/v1/mods/{}?abbreviate=true", data.app_url(), id); let mod_link = format!("{}/mods/{}", data.front_url(), id); - let svg_data_url = format!("data:image/svg+xml;utf8,{}", urlencoding::encode_binary(&svg)); + let svg_data_url = format!( + "data:image/svg+xml;utf8,{}", + urlencoding::encode_binary(&svg) + ); let shields_url = format!( "https://img.shields.io/badge/dynamic/json?url={}&query={}&label={}&labelColor={}&color={}&link={}&style=plastic&logo={}", urlencoding::encode(&api_url), diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index eaf3ad40..630b2084 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -18,10 +18,10 @@ use crate::webhook::discord::DiscordWebhook; use actix_multipart::Multipart; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use futures::StreamExt; -use tracing::error; use serde::Deserialize; use sqlx::{Acquire, PgConnection}; use std::collections::HashMap; +use tracing::error; use utoipa::{IntoParams, ToSchema}; fn sanitize_comment(raw: &str) -> String { @@ -108,7 +108,7 @@ async fn check_submission_lock( #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_submission( path: web::Path, - data: web::Data + data: web::Data, ) -> Result { let mut pool = data.db().acquire().await?; @@ -118,12 +118,15 @@ pub async fn get_submission( return Err(ApiError::NotFound(format!("Mod {} not found", path.id))); } - let version = mod_versions::get_by_version_str(&path.id, &path.version, &mut tx).await? + let version = mod_versions::get_by_version_str(&path.id, &path.version, &mut tx) + .await? .ok_or_else(|| ApiError::NotFound("Version doesn't exist".into()))?; let mut row = mod_version_submissions::get_for_mod_version(version.id, &mut tx).await?; - if let None = row && version.status == ModVersionStatusEnum::Pending { + if let None = row + && version.status == ModVersionStatusEnum::Pending + { row = Some(mod_version_submissions::create(version.id, &mut tx).await?); } @@ -267,7 +270,7 @@ pub async fn update_submission( pub async fn get_comments( path: web::Path, data: web::Data, - query: web::Query + query: web::Query, ) -> Result { let mut pool = data.db().acquire().await?; @@ -329,10 +332,7 @@ pub async fn get_comments( let id = row.id; - Ok(row.into_comment( - author, - attachments_map.remove(&id).unwrap_or_default() - )) + Ok(row.into_comment(author, attachments_map.remove(&id).unwrap_or_default())) }) .collect::, ApiError>>()?; @@ -657,9 +657,10 @@ pub async fn delete_comment( for filename in attachment_filenames { if !references.contains_key(&filename) - && let Err(e) = data.public_storage().delete(&filename).await { - tracing::error!("Failed to delete attachment file {filename}: {e}"); - } + && let Err(e) = data.public_storage().delete(&filename).await + { + tracing::error!("Failed to delete attachment file {filename}: {e}"); + } } Ok(HttpResponse::NoContent()) @@ -682,7 +683,7 @@ pub async fn delete_comment( #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version, comment_id = %path.comment_id))] pub async fn get_attachments( path: web::Path, - data: web::Data + data: web::Data, ) -> Result { let mut pool = data.db().acquire().await?; @@ -727,7 +728,7 @@ pub async fn get_attachments( #[allow(dead_code)] pub struct UploadAttachmentsForm { #[schema(value_type = Vec, format = Binary)] - image: Vec> + image: Vec>, } /// Upload attachments to a submission comment @@ -813,8 +814,7 @@ pub async fn upload_attachments( if existing + count > MAX_ATTACHMENTS_PER_COMMENT { return Err(ApiError::BadRequest(format!( "Comment already has {} attachment(s); adding {} would exceed the limit of 5", - existing, - count + existing, count ))); } let mut buf = bytes::BytesMut::new(); @@ -867,7 +867,8 @@ pub async fn upload_attachments( .await?; stored_filenames.push(filename.clone()); let row = - mod_version_submissions::create_attachment(path.comment_id, &filename, &mut tx).await?; + mod_version_submissions::create_attachment(path.comment_id, &filename, &mut tx) + .await?; result.push(row.into_attachment(data.app_url())); } mod_version_submissions::insert_comment_audit( @@ -888,7 +889,8 @@ pub async fn upload_attachments( .await?; tx.commit().await?; Ok(()) - }.await; + } + .await; if let Err(e) = outcome { for filename in &stored_filenames { diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 6160ff60..93dd2ab6 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -12,7 +12,8 @@ use crate::database::repository::{ }; use crate::endpoints::ApiError; use crate::events::mod_created::{ - NewModAcceptedEvent, NewModVersionAcceptedEvent, NewModVersionVerification, NewUnverifiedModVersionCreated, + NewModAcceptedEvent, NewModVersionAcceptedEvent, NewModVersionVerification, + NewUnverifiedModVersionCreated, }; use crate::mod_zip::{self, download_mod}; use crate::types::models; diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index 06ba35d9..4daa6d5b 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -466,18 +466,19 @@ pub async fn update_mod( let item = Mod::get_one(&id, true, &mut pool).await?; if let Some(item) = item && let Some(owner) = developers::get_owner_for_mod(&id, &mut pool).await? - && let Some(ver) = item.versions.first() { - ModFeaturedEvent { - id: item.id, - name: ver.name.clone(), - owner, - admin: dev, - base_url: data.app_url().to_string(), - featured: payload.featured, - } - .to_discord_webhook() - .send(data.webhook_url()); - } + && let Some(ver) = item.versions.first() + { + ModFeaturedEvent { + id: item.id, + name: ver.name.clone(), + owner, + admin: dev, + base_url: data.app_url().to_string(), + featured: payload.featured, + } + .to_discord_webhook() + .send(data.webhook_url()); + } } Ok(HttpResponse::NoContent()) diff --git a/src/endpoints/stats.rs b/src/endpoints/stats.rs index 65553388..158636ff 100644 --- a/src/endpoints/stats.rs +++ b/src/endpoints/stats.rs @@ -1,24 +1,24 @@ -use actix_web::{get, web, Responder}; - -use super::ApiError; -use crate::config::AppData; -use crate::types::{api::ApiResponse, models::stats::Stats}; - -/// Get global index statistics -#[utoipa::path( - get, - path = "/v1/stats", - tag = "stats", - responses( - (status = 200, description = "Index statistics", body = inline(ApiResponse)) - ) -)] -#[get("/v1/stats")] -#[tracing::instrument(skip_all)] -pub async fn get_stats(data: web::Data) -> Result { - let mut pool = data.db().acquire().await?; - Ok(web::Json(ApiResponse { - error: "".into(), - payload: Stats::get_cached(&mut pool).await?, - })) -} +use actix_web::{Responder, get, web}; + +use super::ApiError; +use crate::config::AppData; +use crate::types::{api::ApiResponse, models::stats::Stats}; + +/// Get global index statistics +#[utoipa::path( + get, + path = "/v1/stats", + tag = "stats", + responses( + (status = 200, description = "Index statistics", body = inline(ApiResponse)) + ) +)] +#[get("/v1/stats")] +#[tracing::instrument(skip_all)] +pub async fn get_stats(data: web::Data) -> Result { + let mut pool = data.db().acquire().await?; + Ok(web::Json(ApiResponse { + error: "".into(), + payload: Stats::get_cached(&mut pool).await?, + })) +} diff --git a/src/endpoints/tags.rs b/src/endpoints/tags.rs index 76352740..234c1e0d 100644 --- a/src/endpoints/tags.rs +++ b/src/endpoints/tags.rs @@ -1,4 +1,4 @@ -use actix_web::{get, web, Responder}; +use actix_web::{Responder, get, web}; use crate::config::AppData; use crate::database::repository::mod_tags; diff --git a/src/events/mod.rs b/src/events/mod.rs index f77d7bfe..2528197e 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -1,3 +1,3 @@ pub mod mod_created; pub mod mod_feature; -pub mod thread_comment; \ No newline at end of file +pub mod thread_comment; diff --git a/src/extractors/auth.rs b/src/extractors/auth.rs index 5e845d0a..959142a0 100644 --- a/src/extractors/auth.rs +++ b/src/extractors/auth.rs @@ -6,7 +6,7 @@ use crate::database::repository::developers; use crate::endpoints::ApiError; use crate::types::models::developer::Developer; use actix_web::http::header::HeaderMap; -use actix_web::{web, FromRequest, HttpRequest}; +use actix_web::{FromRequest, HttpRequest, web}; use futures::Future; use uuid::Uuid; diff --git a/src/jobs/cleanup_downloads.rs b/src/jobs/cleanup_downloads.rs index ec9a42a0..c209c36a 100644 --- a/src/jobs/cleanup_downloads.rs +++ b/src/jobs/cleanup_downloads.rs @@ -7,4 +7,3 @@ pub async fn cleanup_downloads(conn: &mut PgConnection) -> Result<(), ApiError> Ok(()) } - diff --git a/src/jobs/migrate.rs b/src/jobs/migrate.rs index d8c44147..b65dd793 100644 --- a/src/jobs/migrate.rs +++ b/src/jobs/migrate.rs @@ -6,4 +6,4 @@ pub async fn migrate(pool: &mut PgConnection) -> anyhow::Result<()> { } Ok(()) -} \ No newline at end of file +} diff --git a/src/logging.rs b/src/logging.rs index 7936ff56..b21c3c6f 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -14,7 +14,9 @@ pub fn init() { let filter = EnvFilter::from(dotenvy::var("RUST_LOG").unwrap_or(DEFAULT_VALUE.into())); - let is_json = dotenvy::var("LOG_FORMAT").map(|v| v == "json").unwrap_or(false); + let is_json = dotenvy::var("LOG_FORMAT") + .map(|v| v == "json") + .unwrap_or(false); if is_json { tracing_subscriber::fmt() diff --git a/src/main.rs b/src/main.rs index e15082c3..c21a9094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::openapi::ApiDoc; +use crate::storage::StorageDisk; use crate::types::api; use actix_cors::Cors; use actix_web::{ @@ -7,7 +8,6 @@ use actix_web::{ }; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::storage::StorageDisk; mod abbreviate; mod auth; @@ -21,9 +21,9 @@ mod jobs; mod logging; mod mod_zip; mod openapi; +mod storage; mod types; mod webhook; -mod storage; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -68,8 +68,7 @@ async fn main() -> anyhow::Result<()> { .service(actix_files::Files::new("/static", "static")) .service(actix_files::Files::new("/storage", "storage/public")); - app - .service(endpoints::mods::index) + app.service(endpoints::mods::index) .service(endpoints::mods::get_mod_updates) .service(endpoints::mods::get) .service(endpoints::mods::create) diff --git a/src/storage.rs b/src/storage.rs index 83fda493..cd2a3c35 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -3,14 +3,14 @@ use std::path::{Path, PathBuf}; #[derive(Clone, Debug)] pub struct StaticStorage { base_path: PathBuf, - app_url: String + app_url: String, } impl StaticStorage { pub fn new(app_url: String) -> Self { Self { base_path: PathBuf::from("static"), - app_url + app_url, } } @@ -32,14 +32,14 @@ impl StorageDisk for StaticStorage { #[derive(Clone, Debug)] pub struct PublicStorage { base_path: PathBuf, - app_url: String + app_url: String, } impl PublicStorage { pub fn new(app_url: String) -> Self { Self { base_path: PathBuf::from("storage/public"), - app_url + app_url, } } diff --git a/src/types/mod_json.rs b/src/types/mod_json.rs index d3638af5..fc994571 100644 --- a/src/types/mod_json.rs +++ b/src/types/mod_json.rs @@ -247,7 +247,9 @@ impl ModJson { json.about = Some( parse_zip_entry_to_str(&mut file) - .inspect_err(|e| tracing::error!("Failed to parse about.md for mod: {e}")) + .inspect_err(|e| { + tracing::error!("Failed to parse about.md for mod: {e}") + }) .map_err(|e| { ModZipError::InvalidModJson(format!("Failed to read about.md: {e}")) })?, @@ -600,9 +602,15 @@ impl ModJson { } } - if !self.windows && !self.ios && !self.android32 && !self.android64 && !self.mac_intel && !self.mac_arm { + if !self.windows + && !self.ios + && !self.android32 + && !self.android64 + && !self.mac_intel + && !self.mac_arm + { return Err(ModZipError::InvalidModJson( - "Mod has no binaries".to_string() + "Mod has no binaries".to_string(), )); } diff --git a/src/types/models/audit_actions.rs b/src/types/models/audit_actions.rs index 02e77e3f..48ed3b99 100644 --- a/src/types/models/audit_actions.rs +++ b/src/types/models/audit_actions.rs @@ -1,8 +1,8 @@ +use crate::types::serde::chrono_dt_secs; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use utoipa::ToSchema; -use crate::types::serde::chrono_dt_secs; #[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, ToSchema)] #[sqlx(type_name = "audit_action", rename_all = "lowercase")] diff --git a/src/types/models/dependency.rs b/src/types/models/dependency.rs index ae9d827c..0c37d79d 100644 --- a/src/types/models/dependency.rs +++ b/src/types/models/dependency.rs @@ -1,9 +1,9 @@ use std::{collections::HashMap, fmt::Display}; +use crate::database::DatabaseError; use serde::{Deserialize, Serialize}; use sqlx::PgConnection; use utoipa::ToSchema; -use crate::database::DatabaseError; use super::mod_gd_version::{GDVersionEnum, VerPlatform}; @@ -46,7 +46,7 @@ impl FetchedDependency { } }, importance: self.importance, - required: self.importance == DependencyImportance::Required + required: self.importance == DependencyImportance::Required, } } pub fn to_response(&self) -> ResponseDependency { @@ -60,7 +60,7 @@ impl FetchedDependency { } }, importance: self.importance, - required: self.importance == DependencyImportance::Required + required: self.importance == DependencyImportance::Required, } } } diff --git a/src/types/models/developer.rs b/src/types/models/developer.rs index ff28717d..4190e4f8 100644 --- a/src/types/models/developer.rs +++ b/src/types/models/developer.rs @@ -27,7 +27,7 @@ pub struct SelfDeveloper { pub verified: bool, pub admin: bool, pub github_id: i64, - pub has_accepted_mod: bool + pub has_accepted_mod: bool, } impl Developer { @@ -39,7 +39,7 @@ impl Developer { verified: self.verified, admin: self.admin, github_id: self.github_id, - has_accepted_mod + has_accepted_mod, } } } diff --git a/src/types/models/gd_version_alias.rs b/src/types/models/gd_version_alias.rs index 2777b35f..a1ab0bbf 100644 --- a/src/types/models/gd_version_alias.rs +++ b/src/types/models/gd_version_alias.rs @@ -6,7 +6,7 @@ use crate::{ use serde::Serialize; use utoipa::ToSchema; -use sqlx::{types::Uuid, PgConnection, Postgres, QueryBuilder}; +use sqlx::{PgConnection, Postgres, QueryBuilder, types::Uuid}; #[derive(Serialize, ToSchema)] pub struct GDVersionAlias { diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index 9c5880bb..8ad4f3f8 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -1,7 +1,7 @@ use super::{ dependency::{Dependency, ModVersionCompare, ResponseDependency}, - download_count::DownloadCount, developer::ModDeveloper, + download_count::DownloadCount, incompatibility::{Incompatibility, ResponseIncompatibility}, mod_gd_version::{DetailedGDVersion, GDVersionEnum, ModGDVersion, VerPlatform}, mod_version_status::ModVersionStatusEnum, @@ -15,12 +15,12 @@ use crate::types::{ }; use semver::Version; use serde::Serialize; -use utoipa::ToSchema; use sqlx::{ PgConnection, Postgres, QueryBuilder, types::chrono::{DateTime, Utc}, }; use std::collections::HashMap; +use utoipa::ToSchema; #[derive(Serialize, Debug, sqlx::FromRow, Clone, ToSchema)] pub struct ModVersion { diff --git a/src/types/models/mod_version_submission.rs b/src/types/models/mod_version_submission.rs index 03796255..8212c5e4 100644 --- a/src/types/models/mod_version_submission.rs +++ b/src/types/models/mod_version_submission.rs @@ -60,7 +60,7 @@ pub struct ModVersionSubmissionComment { #[derive(Serialize, ToSchema, Debug, Clone)] pub struct ModVersionSubmissionCommentAttachment { pub id: i64, - pub url: String + pub url: String, } pub struct ModVersionSubmissionCommentRow { @@ -83,8 +83,9 @@ impl ModVersionSubmissionCommentRow { submission_id: self.submission_id, comment: self.comment, author, - attachments: attachments.into_iter() - .map(|(id, url)| ModVersionSubmissionCommentAttachment {id, url}) + attachments: attachments + .into_iter() + .map(|(id, url)| ModVersionSubmissionCommentAttachment { id, url }) .collect(), created_at: self.created_at, updated_at: self.updated_at, diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index 0d6f1382..fffea949 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::database::{repository::mod_tags, DatabaseError}; +use crate::database::{DatabaseError, repository::mod_tags}; use crate::endpoints::ApiError; use sqlx::PgConnection; use utoipa::ToSchema; diff --git a/src/types/serde/mod.rs b/src/types/serde/mod.rs index 84893805..c2309256 100644 --- a/src/types/serde/mod.rs +++ b/src/types/serde/mod.rs @@ -1 +1 @@ -pub mod chrono_dt_secs; \ No newline at end of file +pub mod chrono_dt_secs; diff --git a/src/webhook/discord.rs b/src/webhook/discord.rs index f492bbf8..de3bd160 100644 --- a/src/webhook/discord.rs +++ b/src/webhook/discord.rs @@ -23,7 +23,7 @@ impl DiscordMessage { pub fn content(self, content: &str) -> Self { DiscordMessage { embeds: self.embeds, - content: Some(content.into()) + content: Some(content.into()), } } @@ -36,7 +36,7 @@ impl DiscordMessage { if self.embeds.len() == 10 { return DiscordMessage { content: self.content, - embeds: self.embeds + embeds: self.embeds, }; } @@ -61,7 +61,7 @@ impl DiscordMessage { DiscordMessage { content: self.content, - embeds + embeds, } } @@ -76,13 +76,7 @@ impl DiscordMessage { let copy = self.clone(); tokio::spawn(async move { - - if let Err(e) = reqwest::Client::new() - .post(&url) - .json(©) - .send() - .await - { + if let Err(e) = reqwest::Client::new().post(&url).json(©).send().await { tracing::error!("Failed to broadcast Discord webhook {}: {}", url, e); } }); diff --git a/src/webhook/mod.rs b/src/webhook/mod.rs index d598d52f..3228a62a 100644 --- a/src/webhook/mod.rs +++ b/src/webhook/mod.rs @@ -1 +1 @@ -pub mod discord; \ No newline at end of file +pub mod discord; From c16261be1b9c43f9f5ed06fd966a1bd1c19f29ef Mon Sep 17 00:00:00 2001 From: Fleeym Date: Sun, 12 Jul 2026 16:20:23 +0300 Subject: [PATCH 69/69] chore: order cargo.toml --- Cargo.toml | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 18762e26..22cec7b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,15 +3,31 @@ name = "geode-index" version = "0.55.1" edition = "2024" +[features] +dev-tools = ["dep:actix-files"] + [dependencies] -image = { version = "0.25", features = ["png", "jpeg", "webp"] } +actix-cors = "0.7" +actix-files = { version = "0.6", optional = true } actix-multipart = "0.8" actix-web = "4.10" +ammonia = "4" anyhow = "1.0" +bytes = "1.11" +chrono = { version = "0.4", features = ["default", "serde"] } +clap = { version = "4.5", features = ["derive"] } dotenvy = "0.15" futures = "0.3" +image = { version = "0.25", features = ["png", "jpeg", "webp"] } +# Required for zip crate to compile properly +lzma-rust2 = "0.16.5" +moka = { version = "0.12.13", features = ["future"] } +regex = "1.10" +reqwest = { version = "0.13", default-features = false, features = ["json", "gzip", "rustls", "query"] } +semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sha256 = "1.5" sqlx = { version = "0.8", features = [ "postgres", "runtime-tokio", @@ -19,30 +35,14 @@ sqlx = { version = "0.8", features = [ "uuid", "ipnetwork", ] } +thiserror = "2.0.12" tokio = { version = "1.44", features = ["rt", "macros", "rt-multi-thread"] } -reqwest = { version = "0.13", default-features = false, features = ["json", "gzip", "rustls", "query"] } -uuid = { version = "1.8", features = ["v4", "fast-rng", "macro-diagnostics"] } -zip = "8.6.0" -# Required for zip crate to compile properly -lzma-rust2 = "0.16.5" -sha256 = "1.5" -semver = "1.0" -clap = { version = "4.5", features = ["derive"] } -regex = "1.10" -chrono = { version = "0.4", features = ["default", "serde"] } -actix-cors = "0.7" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } tracing-actix-web = "0.7" -thiserror = "2.0.12" -moka = { version = "0.12.13", features = ["future"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } +urlencoding = "2.1.3" utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "uuid"] } utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web"] } -urlencoding = "2.1.3" +uuid = { version = "1.8", features = ["v4", "fast-rng", "macro-diagnostics"] } validator = { version = "0.20.0", features = ["derive"] } -bytes = "1.11" -ammonia = "4" -actix-files = { version = "0.6", optional = true } - -[features] -dev-tools = ["dep:actix-files"] +zip = "8.6.0"