From d2656e8ccafabc387bd37e957d292f38938a160f Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 22 Oct 2026 23:11:39 +0300 Subject: [PATCH 1/8] fix: move endpoints to http module Did a bit of refactoring here: - Moved src/endpoints to src/http/endpoints - Moved everything HTTP related to src/http - Created separate functions for serve, create_app, cors - Split router config across the separate endpoint modules --- src/config.rs | 2 +- src/extractors/auth.rs | 2 +- src/{ => http}/endpoints/auth/github.rs | 33 +++--- src/{ => http}/endpoints/auth/mod.rs | 12 +- src/{ => http}/endpoints/deprecations.rs | 31 +++-- src/{ => http}/endpoints/developers.rs | 69 +++++------ src/{ => http}/endpoints/health.rs | 73 ++++++------ src/{ => http}/endpoints/loader.rs | 21 ++-- src/{ => http}/endpoints/mod.rs | 0 src/{ => http}/endpoints/mod_status_badge.rs | 11 +- .../endpoints/mod_version_submissions.rs | 57 +++++----- src/{ => http}/endpoints/mod_versions.rs | 31 +++-- src/{ => http}/endpoints/mods.rs | 41 ++++--- src/{ => http}/endpoints/stats.rs | 9 +- src/{ => http}/endpoints/tags.rs | 16 +-- src/http/mod.rs | 93 +++++++++++++++ src/jobs/cleanup_downloads.rs | 2 +- src/jobs/logout_user.rs | 2 +- src/jobs/token_cleanup.rs | 2 +- src/main.rs | 107 +----------------- src/openapi.rs | 13 ++- src/types/api.rs | 2 +- src/types/mod_json.rs | 2 +- src/types/models/mod_entity.rs | 4 +- src/types/models/stats.rs | 2 +- src/types/models/tag.rs | 2 +- 26 files changed, 319 insertions(+), 320 deletions(-) rename src/{ => http}/endpoints/auth/github.rs (94%) rename src/{ => http}/endpoints/auth/mod.rs (90%) rename src/{ => http}/endpoints/deprecations.rs (93%) rename src/{ => http}/endpoints/developers.rs (94%) rename src/{ => http}/endpoints/health.rs (93%) rename src/{ => http}/endpoints/loader.rs (94%) rename src/{ => http}/endpoints/mod.rs (100%) rename src/{ => http}/endpoints/mod_status_badge.rs (94%) rename src/{ => http}/endpoints/mod_version_submissions.rs (95%) rename src/{ => http}/endpoints/mod_versions.rs (97%) rename src/{ => http}/endpoints/mods.rs (96%) rename src/{ => http}/endpoints/stats.rs (85%) rename src/{ => http}/endpoints/tags.rs (86%) create mode 100644 src/http/mod.rs diff --git a/src/config.rs b/src/config.rs index 83f8af70..34d02ecf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ use tokio::sync::mpsc::Sender; use crate::{ auth::github::GithubClient, dns::ValidateDnsResolver, - endpoints::mods::IndexQueryParams, + http::endpoints::mods::IndexQueryParams, s3_worker::S3WorkerTask, storage::{LocalBackend, PrivateDisk, PublicDisk, S3Backend, S3Configuration}, types::{ diff --git a/src/extractors/auth.rs b/src/extractors/auth.rs index 959142a0..c41b8cde 100644 --- a/src/extractors/auth.rs +++ b/src/extractors/auth.rs @@ -3,7 +3,7 @@ use std::pin::Pin; use crate::auth::AuthenticationError; use crate::config::AppData; use crate::database::repository::developers; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use crate::types::models::developer::Developer; use actix_web::http::header::HeaderMap; use actix_web::{FromRequest, HttpRequest, web}; diff --git a/src/endpoints/auth/github.rs b/src/http/endpoints/auth/github.rs similarity index 94% rename from src/endpoints/auth/github.rs rename to src/http/endpoints/auth/github.rs index cf27c3cd..fa5b30e3 100644 --- a/src/endpoints/auth/github.rs +++ b/src/http/endpoints/auth/github.rs @@ -9,8 +9,8 @@ use crate::config::AppData; use crate::database::repository::{ auth_tokens, developers, github_login_attempts, github_web_logins, refresh_tokens, }; -use crate::endpoints::ApiError; -use crate::endpoints::auth::TokensResponse; +use crate::http::endpoints::ApiError; +use crate::http::endpoints::auth::TokensResponse; use crate::types::api::ApiResponse; #[derive(Deserialize, ToSchema)] @@ -30,16 +30,23 @@ struct CallbackParams { state: String, } +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(start_github_web_login) + .service(github_web_callback) + .service(poll_github_login) + .service(github_token_login) + .service(start_github_login); +} + /// Start GitHub device flow authentication #[utoipa::path( - post, - path = "/v1/login/github", tag = "auth", responses( (status = 200, description = "Device flow started", body = inline(ApiResponse)) ) )] -#[post("v1/login/github")] +#[post("/login/github")] #[tracing::instrument(skip_all)] pub async fn start_github_login( data: web::Data, @@ -67,14 +74,12 @@ pub async fn start_github_login( /// Start GitHub web OAuth flow #[utoipa::path( - post, - path = "/v1/login/github/web", tag = "auth", responses( (status = 200, description = "OAuth URL generated", body = inline(ApiResponse)) ) )] -#[post("v1/login/github/web")] +#[post("/login/github/web")] #[tracing::instrument(skip_all)] pub async fn start_github_web_login(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; @@ -95,8 +100,6 @@ pub async fn start_github_web_login(data: web::Data) -> Result) -> Result, @@ -149,8 +152,6 @@ pub async fn github_web_callback( /// Poll GitHub device flow for authentication #[utoipa::path( - post, - path = "/v1/login/github/poll", tag = "auth", request_body = PollParams, responses( @@ -159,7 +160,7 @@ pub async fn github_web_callback( (status = 404, description = "Login attempt not found") ) )] -#[post("v1/login/github/poll")] +#[post("/login/github/poll")] #[tracing::instrument(skip_all)] pub async fn poll_github_login( json: web::Json, @@ -252,8 +253,6 @@ pub async fn poll_github_login( /// Login using a GitHub personal access token #[utoipa::path( - post, - path = "/v1/login/github/token", tag = "auth", request_body = TokenLoginParams, responses( @@ -261,7 +260,7 @@ pub async fn poll_github_login( (status = 400, description = "Invalid access token") ) )] -#[post("v1/login/github/token")] +#[post("/login/github/token")] #[tracing::instrument(skip_all)] pub async fn github_token_login( json: web::Json, diff --git a/src/endpoints/auth/mod.rs b/src/http/endpoints/auth/mod.rs similarity index 90% rename from src/endpoints/auth/mod.rs rename to src/http/endpoints/auth/mod.rs index 286a08c2..c6028a59 100644 --- a/src/endpoints/auth/mod.rs +++ b/src/http/endpoints/auth/mod.rs @@ -1,6 +1,6 @@ use crate::config::AppData; use crate::database::repository::{auth_tokens, developers, refresh_tokens}; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use crate::extractors::auth::Auth; use crate::types::api::ApiResponse; use actix_web::{Responder, post, web}; @@ -22,10 +22,14 @@ struct RefreshBody { refresh_token: String, } +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(refresh_token) + .configure(github::router_config); +} + /// Refresh an access token using a refresh token #[utoipa::path( - post, - path = "/v1/login/refresh", tag = "auth", request_body = RefreshBody, responses( @@ -33,7 +37,7 @@ struct RefreshBody { (status = 400, description = "Invalid or expired refresh token") ) )] -#[post("v1/login/refresh")] +#[post("/login/refresh")] #[tracing::instrument(skip_all)] pub async fn refresh_token( json: web::Json, diff --git a/src/endpoints/deprecations.rs b/src/http/endpoints/deprecations.rs similarity index 93% rename from src/endpoints/deprecations.rs rename to src/http/endpoints/deprecations.rs index 1d06cab6..c9bb8e48 100644 --- a/src/endpoints/deprecations.rs +++ b/src/http/endpoints/deprecations.rs @@ -1,7 +1,7 @@ use crate::{ config::AppData, database::repository::{deprecations, developers, mods}, - endpoints::ApiError, + http::endpoints::ApiError, extractors::auth::Auth, types::api::ApiResponse, types::models::deprecations::Deprecation, @@ -36,10 +36,17 @@ struct UpdateDeprecationData { const MAX_MODS_PER_DEPRECATION: usize = 20; +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(index) + .service(store) + .service(update) + .service(clear_all) + .service(delete); +} + /// Fetch all deprecations for a mod #[utoipa::path( - get, - path = "/v1/mods/{id}/deprecations", tag = "deprecations", params(ModPath), responses( @@ -47,7 +54,7 @@ const MAX_MODS_PER_DEPRECATION: usize = 20; (status = 404, description = "Mod not found") ) )] -#[get("v1/mods/{id}/deprecations")] +#[get("/mods/{id}/deprecations")] #[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn index( data: web::Data, @@ -68,8 +75,6 @@ pub async fn index( /// Insert one deprecation for a mod #[utoipa::path( - post, - path = "/v1/mods/{id}/deprecations", tag = "deprecations", params(ModPath), request_body = CreateDeprecationData, @@ -84,7 +89,7 @@ pub async fn index( ("bearer_token" = []) ) )] -#[post("v1/mods/{id}/deprecations")] +#[post("/mods/{id}/deprecations")] #[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn store( data: web::Data, @@ -117,8 +122,6 @@ pub async fn store( /// Update a deprecation #[utoipa::path( - put, - path = "/v1/mods/{id}/deprecations/{deprecation_id}", tag = "deprecations", params(ModDeprecationPath), request_body = UpdateDeprecationData, @@ -132,7 +135,7 @@ pub async fn store( ("bearer_token" = []) ) )] -#[put("v1/mods/{id}/deprecations/{deprecation_id}")] +#[put("/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, @@ -189,8 +192,6 @@ pub async fn update( /// Delete a deprecation #[utoipa::path( - delete, - path = "/v1/mods/{id}/deprecations/{deprecation_id}", tag = "deprecations", params(ModDeprecationPath), responses( @@ -203,7 +204,7 @@ pub async fn update( ("bearer_token" = []) ) )] -#[delete("v1/mods/{id}/deprecations/{deprecation_id}")] +#[delete("/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, @@ -243,8 +244,6 @@ pub async fn delete( /// Delete all deprecations for a mod #[utoipa::path( - delete, - path = "/v1/mods/{id}/deprecations", tag = "deprecations", params(ModPath), responses( @@ -257,7 +256,7 @@ pub async fn delete( ("bearer_token" = []) ) )] -#[delete("v1/mods/{id}/deprecations")] +#[delete("/mods/{id}/deprecations")] #[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn clear_all( data: web::Data, diff --git a/src/endpoints/developers.rs b/src/http/endpoints/developers.rs similarity index 94% rename from src/endpoints/developers.rs rename to src/http/endpoints/developers.rs index 24bf7389..c5d8b7a1 100644 --- a/src/endpoints/developers.rs +++ b/src/http/endpoints/developers.rs @@ -77,17 +77,32 @@ struct DeveloperBanPayload { revoked_at: Option>, } +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(developer_index) + .service(get_developer) + .service(add_developer_to_mod) + .service(remove_dev_from_mod) + .service(delete_token) + .service(delete_tokens) + .service(update_profile) + .service(get_own_mods) + .service(get_me) + .service(update_developer) + .service(ban_developer) + .service(unban_developer) + .service(get_developer_ban); +} + /// List all developers with optional search and pagination #[utoipa::path( - get, - path = "/v1/developers", tag = "developers", params(DeveloperIndexQuery), responses( (status = 200, description = "List of developers", body = inline(ApiResponse>)) ) )] -#[get("v1/developers")] +#[get("/developers")] #[tracing::instrument(skip_all)] pub async fn developer_index( data: web::Data, @@ -108,8 +123,6 @@ pub async fn developer_index( /// Add a developer to a mod #[utoipa::path( - post, - path = "/v1/mods/{id}/developers", tag = "developers", params(AddDevPath), request_body = AddDevPayload, @@ -124,7 +137,7 @@ pub async fn developer_index( ("bearer_token" = []) ) )] -#[post("v1/mods/{id}/developers")] +#[post("/mods/{id}/developers")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, username = %json.username))] pub async fn add_developer_to_mod( data: web::Data, @@ -164,8 +177,6 @@ pub async fn add_developer_to_mod( /// Remove a developer from a mod #[utoipa::path( - delete, - path = "/v1/mods/{id}/developers/{username}", tag = "developers", params(RemoveDevPath), responses( @@ -179,7 +190,7 @@ pub async fn add_developer_to_mod( ("bearer_token" = []) ) )] -#[delete("v1/mods/{id}/developers/{username}")] +#[delete("/mods/{id}/developers/{username}")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, username = %path.username))] pub async fn remove_dev_from_mod( data: web::Data, @@ -225,8 +236,6 @@ pub async fn remove_dev_from_mod( /// Delete the current API token #[utoipa::path( - delete, - path = "/v1/me/token", tag = "developers", responses( (status = 204, description = "Token deleted successfully"), @@ -236,7 +245,7 @@ pub async fn remove_dev_from_mod( ("bearer_token" = []) ) )] -#[delete("v1/me/token")] +#[delete("/me/token")] #[tracing::instrument(skip_all)] pub async fn delete_token( data: web::Data, @@ -252,8 +261,6 @@ pub async fn delete_token( /// Delete all API tokens for the current developer #[utoipa::path( - delete, - path = "/v1/me/tokens", tag = "developers", responses( (status = 204, description = "All tokens deleted successfully"), @@ -263,7 +270,7 @@ pub async fn delete_token( ("bearer_token" = []) ) )] -#[delete("v1/me/tokens")] +#[delete("/me/tokens")] #[tracing::instrument(skip_all)] pub async fn delete_tokens( data: web::Data, @@ -285,8 +292,6 @@ struct UploadProfilePayload { /// Update the current developer's profile #[utoipa::path( - put, - path = "/v1/me", tag = "developers", request_body = UploadProfilePayload, responses( @@ -298,7 +303,7 @@ struct UploadProfilePayload { ("bearer_token" = []) ) )] -#[put("v1/me")] +#[put("/me")] #[tracing::instrument(skip_all)] pub async fn update_profile( data: web::Data, @@ -344,8 +349,6 @@ pub fn default_own_mods_status() -> ModVersionStatusEnum { /// Get all mods owned by the current developer #[utoipa::path( - get, - path = "/v1/me/mods", tag = "developers", params(GetOwnModsQuery), responses( @@ -356,7 +359,7 @@ pub fn default_own_mods_status() -> ModVersionStatusEnum { ("bearer_token" = []) ) )] -#[get("v1/me/mods")] +#[get("/me/mods")] #[tracing::instrument(skip_all)] pub async fn get_own_mods( data: web::Data, @@ -375,8 +378,6 @@ pub async fn get_own_mods( /// Get the current developer's profile #[utoipa::path( - get, - path = "/v1/me", tag = "developers", responses( (status = 200, description = "Current developer profile", body = inline(ApiResponse)), @@ -386,7 +387,7 @@ pub async fn get_own_mods( ("bearer_token" = []) ) )] -#[get("v1/me")] +#[get("/me")] #[tracing::instrument(skip_all)] pub async fn get_me(auth: Auth, data: web::Data) -> Result { let mut pool = data.db().acquire().await?; @@ -407,8 +408,6 @@ struct GetDeveloperPath { /// Get a specific developer by ID #[utoipa::path( - get, - path = "/v1/developers/{id}", tag = "developers", params(GetDeveloperPath), responses( @@ -416,7 +415,7 @@ struct GetDeveloperPath { (status = 404, description = "Developer not found") ) )] -#[get("v1/developers/{id}")] +#[get("/developers/{id}")] #[tracing::instrument(skip_all, fields(developer_id = %path.id))] pub async fn get_developer( data: web::Data, @@ -435,8 +434,6 @@ pub async fn get_developer( /// Update a developer's admin/verified status (admin only) #[utoipa::path( - put, - path = "/v1/developers/{id}", tag = "developers", params(UpdateDeveloperPath), request_body = DeveloperUpdatePayload, @@ -451,7 +448,7 @@ pub async fn get_developer( ("bearer_token" = []) ) )] -#[put("v1/developers/{id}")] +#[put("/developers/{id}")] #[tracing::instrument(skip_all, fields(developer_id = %path.id))] pub async fn update_developer( auth: Auth, @@ -510,8 +507,6 @@ struct CreateDeveloperBanPath { /// /// If the developer is already banned, this will overwrite that ban. #[utoipa::path( - post, - path = "/v1/developers/{id}/bans", tag = "developers", params(CreateDeveloperBanPath), request_body = DeveloperBanPayload, @@ -526,7 +521,7 @@ struct CreateDeveloperBanPath { ("bearer_token" = []) ) )] -#[post("/v1/developers/{id}/bans")] +#[post("/developers/{id}/bans")] pub async fn ban_developer( auth: Auth, data: web::Data, @@ -587,8 +582,6 @@ struct DeleteDeveloperBanPath { /// Revoke a developer's current ban (admin only) #[utoipa::path( - delete, - path = "/v1/developers/{id}/bans", tag = "developers", params(DeleteDeveloperBanPath), responses( @@ -601,7 +594,7 @@ struct DeleteDeveloperBanPath { ("bearer_token" = []) ) )] -#[delete("/v1/developers/{id}/bans")] +#[delete("/developers/{id}/bans")] pub async fn unban_developer( auth: Auth, data: web::Data, @@ -627,8 +620,6 @@ struct GetDeveloperBanPath { /// Fetch a list of a developer's bans (admin only) #[utoipa::path( - get, - path = "/v1/developers/{id}/bans", tag = "developers", params(GetDeveloperBanPath), responses( @@ -642,7 +633,7 @@ struct GetDeveloperBanPath { ("bearer_token" = []) ) )] -#[get("/v1/developers/{id}/bans")] +#[get("/developers/{id}/bans")] pub async fn get_developer_ban( auth: Auth, data: web::Data, diff --git a/src/endpoints/health.rs b/src/http/endpoints/health.rs similarity index 93% rename from src/endpoints/health.rs rename to src/http/endpoints/health.rs index b97aec9f..4e80ca3f 100644 --- a/src/endpoints/health.rs +++ b/src/http/endpoints/health.rs @@ -1,35 +1,38 @@ -use actix_web::route; - -/// Health check endpoint -#[utoipa::path( - get, - path = "/", - tag = "health", - responses( - (status = 200, description = "Service is healthy", body = String, content_type = "text/plain") - ) -)] -#[route("/", method = "GET", method = "HEAD")] -#[tracing::instrument(skip_all)] -pub async fn health() -> &'static str { - r#" - _____ _____ -( ___ )----------------------------------------------------------------------( ___ ) - | | | | - | | _ _ _ _ _ | | - | | | | | (_) ___| |_ _ __ __ _ _ __ __ _ ___ _ __| | | | - | | | |_| | | / __| __| '__/ _` | '_ \ / _` |/ _ \ '__| | | | - | | | _ | |_ \__ \ |_| | | (_| | | | | (_| | __/ | |_| | | - | | |_| |_|_( )_|___/\__|_| \__,_|_| |_|\__, |\___|_| (_)_ _ | | - | | \ \ |/ /__| | ___ ___ _ __ ___ |___/ | |_ ___ | |_| |__ ___ | | - | | \ \ /\ / / _ \ |/ __/ _ \| '_ ` _ \ / _ \ | __/ _ \ | __| '_ \ / _ \ | | - | | \ V V / __/ | (_| (_) | | | | | | __/ | || (_) | | |_| | | | __/ | | - | | _\_/\_/ \___|_|\___\___/|_| |_| |_|\___| _\__\___/ \__|_| |_|\___| | | - | | / ___| ___ ___ __| | ___ (_)_ __ __| | _____ _| | | | - | | | | _ / _ \/ _ \ / _` |/ _ \ | | '_ \ / _` |/ _ \ \/ / | | | - | | | |_| | __/ (_) | (_| | __/ | | | | | (_| | __/> <|_| | | - | | \____|\___|\___/ \__,_|\___| |_|_| |_|\__,_|\___/_/\_(_) | | - |___| |___| -(_____)----------------------------------------------------------------------(_____) -"# -} +use actix_web::{route, web}; + +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(health); +} + +/// Health check endpoint +#[utoipa::path( + tag = "health", + responses( + (status = 200, description = "Service is healthy", body = String, content_type = "text/plain") + ) +)] +#[route("/", method = "GET", method = "HEAD")] +#[tracing::instrument(skip_all)] +pub async fn health() -> &'static str { + r#" + _____ _____ +( ___ )----------------------------------------------------------------------( ___ ) + | | | | + | | _ _ _ _ _ | | + | | | | | (_) ___| |_ _ __ __ _ _ __ __ _ ___ _ __| | | | + | | | |_| | | / __| __| '__/ _` | '_ \ / _` |/ _ \ '__| | | | + | | | _ | |_ \__ \ |_| | | (_| | | | | (_| | __/ | |_| | | + | | |_| |_|_( )_|___/\__|_| \__,_|_| |_|\__, |\___|_| (_)_ _ | | + | | \ \ |/ /__| | ___ ___ _ __ ___ |___/ | |_ ___ | |_| |__ ___ | | + | | \ \ /\ / / _ \ |/ __/ _ \| '_ ` _ \ / _ \ | __/ _ \ | __| '_ \ / _ \ | | + | | \ V V / __/ | (_| (_) | | | | | | __/ | || (_) | | |_| | | | __/ | | + | | _\_/\_/ \___|_|\___\___/|_| |_| |_|\___| _\__\___/ \__|_| |_|\___| | | + | | / ___| ___ ___ __| | ___ (_)_ __ __| | _____ _| | | | + | | | | _ / _ \/ _ \ / _` |/ _ \ | | '_ \ / _` |/ _ \ \/ / | | | + | | | |_| | __/ (_) | (_| | __/ | | | | | (_| | __/> <|_| | | + | | \____|\___|\___/ \__,_|\___| |_|_| |_|\__,_|\___/_/\_(_) | | + |___| |___| +(_____)----------------------------------------------------------------------(_____) +"# +} diff --git a/src/endpoints/loader.rs b/src/http/endpoints/loader.rs similarity index 94% rename from src/endpoints/loader.rs rename to src/http/endpoints/loader.rs index 56cd664c..06fbc448 100644 --- a/src/endpoints/loader.rs +++ b/src/http/endpoints/loader.rs @@ -5,7 +5,7 @@ use utoipa::{IntoParams, ToSchema}; use sqlx::Acquire; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use crate::s3_worker::S3WorkerTask; use crate::{ config::AppData, @@ -33,10 +33,15 @@ struct GetOnePath { version: String, } +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(get_one) + .service(create_version) + .service(get_many); +} + /// Get a specific loader version (or latest) #[utoipa::path( - get, - path = "/v1/loader/versions/{version}", tag = "loader", params(GetOnePath, GetOneQuery), responses( @@ -44,7 +49,7 @@ struct GetOnePath { (status = 404, description = "Version not found") ) )] -#[get("v1/loader/versions/{version}")] +#[get("/loader/versions/{version}")] #[tracing::instrument(skip_all, fields(version = %path.version))] pub async fn get_one( path: web::Path, @@ -95,8 +100,6 @@ struct CreateVersionBody { /// Create a new loader version (admin only) #[utoipa::path( - post, - path = "/v1/loader/versions", tag = "loader", request_body = CreateVersionBody, responses( @@ -109,7 +112,7 @@ struct CreateVersionBody { ("bearer_token" = []) ) )] -#[post("v1/loader/versions")] +#[post("/loader/versions")] #[tracing::instrument(skip_all, fields(tag = %payload.tag))] pub async fn create_version( data: web::Data, @@ -158,15 +161,13 @@ struct GetManyQuery { /// Get all loader versions with optional filtering #[utoipa::path( - get, - path = "/v1/loader/versions", tag = "loader", params(GetManyQuery), responses( (status = 200, description = "List of loader versions", body = inline(ApiResponse>)) ) )] -#[get("v1/loader/versions")] +#[get("/loader/versions")] #[tracing::instrument(skip_all)] pub async fn get_many( data: web::Data, diff --git a/src/endpoints/mod.rs b/src/http/endpoints/mod.rs similarity index 100% rename from src/endpoints/mod.rs rename to src/http/endpoints/mod.rs diff --git a/src/endpoints/mod_status_badge.rs b/src/http/endpoints/mod_status_badge.rs similarity index 94% rename from src/endpoints/mod_status_badge.rs rename to src/http/endpoints/mod_status_badge.rs index 1112aad8..cd7f7095 100644 --- a/src/endpoints/mod_status_badge.rs +++ b/src/http/endpoints/mod_status_badge.rs @@ -1,5 +1,5 @@ use crate::config::AppData; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use actix_web::{HttpResponse, Responder, get, web}; use serde::Deserialize; use utoipa::{IntoParams, ToSchema}; @@ -23,9 +23,12 @@ pub struct StatusBadgeQuery { pub stat: StatusBadgeStat, } +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(status_badge); +} + #[utoipa::path( - get, - path = "/v1/mods/{id}/status_badge", tag = "mods", params( ("id" = String, Path, description = "Mod ID"), @@ -37,7 +40,7 @@ pub struct StatusBadgeQuery { (status = 404, description = "Mod not found") ) )] -#[get("/v1/mods/{id}/status_badge")] +#[get("/mods/{id}/status_badge")] #[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn status_badge( data: web::Data, diff --git a/src/endpoints/mod_version_submissions.rs b/src/http/endpoints/mod_version_submissions.rs similarity index 95% rename from src/endpoints/mod_version_submissions.rs rename to src/http/endpoints/mod_version_submissions.rs index 23126149..f178caaa 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/http/endpoints/mod_version_submissions.rs @@ -95,10 +95,23 @@ async fn check_submission_lock( }) } +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(get_submission) + .service(get_submission_audit) + .service(update_submission) + .service(get_comments) + .service(get_comment_audit) + .service(create_comment) + .service(update_comment) + .service(delete_comment) + .service(get_attachments) + .service(upload_attachments) + .service(delete_attachment); +} + /// 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", tag = "mod_version_submissions", params(SubmissionPath), responses( @@ -108,7 +121,7 @@ async fn check_submission_lock( ), security(("bearer_token" = [])) )] -#[get("v1/mods/{id}/versions/{version}/submission")] +#[get("/mods/{id}/versions/{version}/submission")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_submission( path: web::Path, @@ -154,7 +167,6 @@ pub async fn get_submission( } #[utoipa::path( - path = "/v1/mods/{id}/versions/{version}/submission/audit", tag = "mod_version_submissions", params(SubmissionPath), responses( @@ -165,7 +177,7 @@ pub async fn get_submission( ), security(("bearer_token" = [])) )] -#[get("v1/mods/{id}/versions/{version}/submission/audit")] +#[get("/mods/{id}/versions/{version}/submission/audit")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_submission_audit( path: web::Path, @@ -197,8 +209,6 @@ pub async fn get_submission_audit( /// 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, @@ -210,7 +220,7 @@ pub async fn get_submission_audit( ), security(("bearer_token" = [])) )] -#[put("v1/mods/{id}/versions/{version}/submission")] +#[put("/mods/{id}/versions/{version}/submission")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn update_submission( path: web::Path, @@ -258,8 +268,6 @@ pub async fn update_submission( /// 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( @@ -269,7 +277,7 @@ pub async fn update_submission( ), security(("bearer_token" = [])) )] -#[get("v1/mods/{id}/versions/{version}/submission/comments")] +#[get("/mods/{id}/versions/{version}/submission/comments")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_comments( path: web::Path, @@ -350,7 +358,6 @@ 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( @@ -361,7 +368,7 @@ pub async fn get_comments( ), security(("bearer_token" = [])) )] -#[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/audit")] +#[get("/mods/{id}/versions/{version}/submission/comments/{comment_id}/audit")] #[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, @@ -400,8 +407,6 @@ pub async fn get_comment_audit( /// 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, @@ -413,7 +418,7 @@ pub async fn get_comment_audit( ), security(("bearer_token" = [])) )] -#[post("v1/mods/{id}/versions/{version}/submission/comments")] +#[post("/mods/{id}/versions/{version}/submission/comments")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn create_comment( path: web::Path, @@ -482,8 +487,6 @@ pub async fn create_comment( /// 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, @@ -496,7 +499,7 @@ pub async fn create_comment( ), security(("bearer_token" = [])) )] -#[put("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] +#[put("/mods/{id}/versions/{version}/submission/comments/{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, @@ -584,8 +587,6 @@ pub async fn update_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( @@ -596,7 +597,7 @@ pub async fn update_comment( ), security(("bearer_token" = [])) )] -#[delete("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}")] +#[delete("/mods/{id}/versions/{version}/submission/comments/{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, @@ -672,8 +673,6 @@ pub async fn delete_comment( /// 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( @@ -683,7 +682,7 @@ pub async fn delete_comment( ), security(("bearer_token" = [])) )] -#[get("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] +#[get("/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] #[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, @@ -737,8 +736,6 @@ pub struct UploadAttachmentsForm { /// 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( @@ -754,7 +751,7 @@ pub struct UploadAttachmentsForm { ), security(("bearer_token" = [])) )] -#[post("v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] +#[post("/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments")] #[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, @@ -916,8 +913,6 @@ pub async fn upload_attachments( /// 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( @@ -929,7 +924,7 @@ pub async fn upload_attachments( security(("bearer_token" = [])) )] #[delete( - "v1/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments/{attachment_id}" + "/mods/{id}/versions/{version}/submission/comments/{comment_id}/attachments/{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( diff --git a/src/endpoints/mod_versions.rs b/src/http/endpoints/mod_versions.rs similarity index 97% rename from src/endpoints/mod_versions.rs rename to src/http/endpoints/mod_versions.rs index 70c807d5..a3f378fb 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/http/endpoints/mod_versions.rs @@ -11,7 +11,7 @@ use crate::database::repository::{ dependencies, developers, incompatibilities, mod_downloads, mod_gd_versions, mod_links, mod_tags, mod_version_submissions, mod_versions, mods, }; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use crate::events::mod_created::{ NewModAcceptedEvent, NewModVersionAcceptedEvent, NewModVersionVerification, NewUnverifiedModVersionCreated, @@ -81,10 +81,17 @@ struct IndexQuery { compare: Option, } +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(get_version_index) + .service(get_one) + .service(download_version) + .service(create_version) + .service(update_version); +} + /// List all versions for a mod #[utoipa::path( - get, - path = "/v1/mods/{id}/versions", tag = "mod_versions", params(IndexPath, IndexQuery), responses( @@ -92,7 +99,7 @@ struct IndexQuery { (status = 404, description = "Mod not found") ) )] -#[get("v1/mods/{id}/versions")] +#[get("/mods/{id}/versions")] #[tracing::instrument(skip_all, fields(mod_id = %path.id))] pub async fn get_version_index( path: web::Path, @@ -144,8 +151,6 @@ pub async fn get_version_index( /// Get a specific version of a mod #[utoipa::path( - get, - path = "/v1/mods/{id}/versions/{version}", tag = "mod_versions", params(GetOnePath, GetOneQuery), responses( @@ -153,7 +158,7 @@ pub async fn get_version_index( (status = 404, description = "Mod or version not found") ) )] -#[get("v1/mods/{id}/versions/{version}")] +#[get("/mods/{id}/versions/{version}")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn get_one( path: web::Path, @@ -207,8 +212,6 @@ struct DownloadQuery { /// Download a specific version of a mod (redirects to download URL) #[utoipa::path( - get, - path = "/v1/mods/{id}/versions/{version}/download", tag = "mod_versions", params(GetOnePath, DownloadQuery), responses( @@ -216,7 +219,7 @@ struct DownloadQuery { (status = 404, description = "Mod or version not found") ) )] -#[get("v1/mods/{id}/versions/{version}/download")] +#[get("/mods/{id}/versions/{version}/download")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn download_version( path: web::Path, @@ -286,8 +289,6 @@ pub async fn download_version( /// Create a new version for a mod #[utoipa::path( - post, - path = "/v1/mods/{id}/versions", tag = "mod_versions", params( ("id" = String, Path, description = "Mod ID") @@ -304,7 +305,7 @@ pub async fn download_version( ("bearer_token" = []) ) )] -#[post("v1/mods/{id}/versions")] +#[post("/mods/{id}/versions")] #[tracing::instrument(skip_all, fields(mod_id = %path))] pub async fn create_version( path: web::Path, @@ -515,8 +516,6 @@ pub async fn create_version( /// Update a mod version status (admin only) #[utoipa::path( - put, - path = "/v1/mods/{id}/versions/{version}", tag = "mod_versions", params(UpdateVersionPath), request_body = UpdatePayload, @@ -531,7 +530,7 @@ pub async fn create_version( ("bearer_token" = []) ) )] -#[put("v1/mods/{id}/versions/{version}")] +#[put("/mods/{id}/versions/{version}")] #[tracing::instrument(skip_all, fields(mod_id = %path.id, version = %path.version))] pub async fn update_version( path: web::Path, diff --git a/src/endpoints/mods.rs b/src/http/endpoints/mods.rs similarity index 96% rename from src/endpoints/mods.rs rename to src/http/endpoints/mods.rs index b6ef0f13..c407494e 100644 --- a/src/endpoints/mods.rs +++ b/src/http/endpoints/mods.rs @@ -12,7 +12,7 @@ use crate::database::repository::mod_versions; use crate::database::repository::mods; use crate::database::repository::mods::ModLogo; use crate::database::repository::{dependencies, deprecations, mod_version_submissions}; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use crate::events::mod_created::NewUnverifiedModVersionCreated; use crate::events::mod_feature::ModFeaturedEvent; use crate::extractors::auth::Auth; @@ -73,10 +73,19 @@ pub struct CreateQueryParams { download_link: String, } +// `get_mod_updates` must be registered before `get`, or "updates" is captured as `{id}` +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(index) + .service(get_mod_updates) + .service(get) + .service(create) + .service(update_mod) + .service(get_logo); +} + /// List all mods with optional filtering and pagination #[utoipa::path( - get, - path = "/v1/mods", tag = "mods", params(IndexQueryParams), responses( @@ -85,7 +94,7 @@ pub struct CreateQueryParams { (status = 403, description = "Forbidden") ) )] -#[get("/v1/mods")] +#[get("/mods")] #[tracing::instrument(skip_all)] pub async fn index( data: web::Data, @@ -127,8 +136,6 @@ pub async fn index( /// Get a specific mod by ID #[utoipa::path( - get, - path = "/v1/mods/{id}", tag = "mods", params( ("id" = String, Path, description = "Mod ID") @@ -138,7 +145,7 @@ pub async fn index( (status = 404, description = "Mod not found") ) )] -#[get("/v1/mods/{id}")] +#[get("/mods/{id}")] #[tracing::instrument(skip_all, fields(mod_id = %id))] pub async fn get( data: web::Data, @@ -199,8 +206,6 @@ pub async fn get( /// Create a new mod #[utoipa::path( - post, - path = "/v1/mods", tag = "mods", request_body = CreateQueryParams, responses( @@ -213,7 +218,7 @@ pub async fn get( ("bearer_token" = []) ) )] -#[post("/v1/mods")] +#[post("/mods")] #[tracing::instrument(skip_all)] pub async fn create( data: web::Data, @@ -234,11 +239,11 @@ pub async fn create( json.validate()?; let mut pool = data.db().acquire().await?; - + if let Some(ban) = developers::check_ban(dev.id, &mut pool).await? { return Err(ApiError::Banned(ban.reason)); } - + let existing: Option = mods::get_one(&json.id, false, &mut pool).await?; if json.id.starts_with("geode.") && !dev.admin { @@ -351,8 +356,6 @@ enum UpdateQueryResponse { /// Get available mod updates for a list of installed mods #[utoipa::path( - get, - path = "/v1/mods/updates", tag = "mods", params(UpdateQueryParams), responses( @@ -360,7 +363,7 @@ enum UpdateQueryResponse { (status = 400, description = "Bad request") ) )] -#[get("/v1/mods/updates")] +#[get("/mods/updates")] #[tracing::instrument(skip_all, fields(ids = %query.ids, geode = %query.geode))] pub async fn get_mod_updates( data: web::Data, @@ -413,8 +416,6 @@ pub async fn get_mod_updates( /// Get the logo image for a mod #[utoipa::path( - get, - path = "/v1/mods/{id}/logo", tag = "mods", params( ("id" = String, Path, description = "Mod ID") @@ -424,7 +425,7 @@ pub async fn get_mod_updates( (status = 404, description = "Logo not found") ) )] -#[get("/v1/mods/{id}/logo")] +#[get("/mods/{id}/logo")] #[tracing::instrument(skip_all, fields(mod_id = %path))] pub async fn get_logo( data: web::Data, @@ -450,8 +451,6 @@ struct UpdateModPayload { /// Update a mod (admin only) #[utoipa::path( - put, - path = "/v1/mods/{id}", tag = "mods", params( ("id" = String, Path, description = "Mod ID") @@ -467,7 +466,7 @@ struct UpdateModPayload { ("bearer_token" = []) ) )] -#[put("/v1/mods/{id}")] +#[put("/mods/{id}")] #[tracing::instrument(skip_all, fields(mod_id = %path))] pub async fn update_mod( data: web::Data, diff --git a/src/endpoints/stats.rs b/src/http/endpoints/stats.rs similarity index 85% rename from src/endpoints/stats.rs rename to src/http/endpoints/stats.rs index 8ca6dea0..5b2afffc 100644 --- a/src/endpoints/stats.rs +++ b/src/http/endpoints/stats.rs @@ -4,16 +4,19 @@ use super::ApiError; use crate::config::AppData; use crate::types::{api::ApiResponse, models::stats::Stats}; +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(get_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")] +#[get("/stats")] #[tracing::instrument(skip_all)] pub async fn get_stats(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; diff --git a/src/endpoints/tags.rs b/src/http/endpoints/tags.rs similarity index 86% rename from src/endpoints/tags.rs rename to src/http/endpoints/tags.rs index 234c1e0d..03cded4f 100644 --- a/src/endpoints/tags.rs +++ b/src/http/endpoints/tags.rs @@ -2,20 +2,24 @@ use actix_web::{Responder, get, web}; use crate::config::AppData; use crate::database::repository::mod_tags; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use crate::types::api::ApiResponse; use crate::types::models::tag::Tag; +pub fn router_config(cfg: &mut web::ServiceConfig) { + cfg + .service(index) + .service(detailed_index); +} + /// Get all available tags #[utoipa::path( - get, - path = "/v1/tags", tag = "tags", responses( (status = 200, description = "List of tag names", body = inline(ApiResponse>)) ) )] -#[get("/v1/tags")] +#[get("/tags")] #[tracing::instrument(skip_all)] pub async fn index(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; @@ -33,14 +37,12 @@ pub async fn index(data: web::Data) -> Result /// Get all available tags with detailed information #[utoipa::path( - get, - path = "/v1/detailed-tags", tag = "tags", responses( (status = 200, description = "List of tags with details", body = inline(ApiResponse>)) ) )] -#[get("/v1/detailed-tags")] +#[get("/detailed-tags")] #[tracing::instrument(skip_all)] pub async fn detailed_index(data: web::Data) -> Result { let mut pool = data.db().acquire().await?; diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 00000000..64c3f785 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,93 @@ +use actix_cors::Cors; +use actix_web::{ + App, HttpServer, + body::MessageBody, + dev::{ServiceFactory, ServiceRequest, ServiceResponse}, + web::{self, QueryConfig}, +}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::{ + config::AppData, + http::endpoints::{ + auth, deprecations, developers, health, loader, mod_status_badge, mod_version_submissions, + mod_versions, mods, stats, tags, + }, + openapi::ApiDoc, + types::api, +}; + +pub mod endpoints; + +fn router_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/v1") + .configure(mods::router_config) + .configure(mod_status_badge::router_config) + .configure(mod_versions::router_config) + .configure(mod_version_submissions::router_config) + .configure(deprecations::router_config) + .configure(auth::router_config) + .configure(developers::router_config) + .configure(tags::router_config) + .configure(stats::router_config) + .configure(loader::router_config), + ) + .configure(health::router_config); +} + +pub fn cors_settings() -> Cors { + Cors::default() + .allow_any_origin() + .allowed_methods(vec!["GET", "HEAD"]) + .allowed_headers(vec![actix_web::http::header::ACCEPT]) + .max_age(3600) +} + +pub fn create_app( + data: web::Data, + openapi: utoipa::openapi::OpenApi, +) -> App< + impl ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = actix_web::Error, + InitError = (), + >, +> { + let app = App::new() + .app_data(data.clone()) + .app_data(QueryConfig::default().error_handler(api::query_error_handler)) + .service(SwaggerUi::new("/swagger/{_:.*}").url("/swagger/openapi.json", openapi.clone())) + .wrap(cors_settings()) + .wrap(tracing_actix_web::TracingLogger::default()); + + #[cfg(feature = "serve-static")] + let app = app + .service(actix_files::Files::new("/static", "static")) + .service(actix_files::Files::new("/storage", "storage/public")); + + app.configure(router_config) +} + +pub async fn serve_http(data: web::Data) -> anyhow::Result<()> { + let port = data.port(); + let debug = data.debug(); + + tracing::info!("Starting server on 0.0.0.0:{}", port); + + let openapi = ApiDoc::openapi(); + let server = HttpServer::new(move || create_app(data.clone(), openapi.clone())) + .bind(("0.0.0.0", port))?; + + if debug { + tracing::info!("Running in debug mode, using 1 thread."); + server.workers(1).run().await?; + } else { + server.run().await?; + } + + Ok(()) +} diff --git a/src/jobs/cleanup_downloads.rs b/src/jobs/cleanup_downloads.rs index c209c36a..5dc78afc 100644 --- a/src/jobs/cleanup_downloads.rs +++ b/src/jobs/cleanup_downloads.rs @@ -1,5 +1,5 @@ use crate::database::repository::mod_downloads; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use sqlx::PgConnection; pub async fn cleanup_downloads(conn: &mut PgConnection) -> Result<(), ApiError> { diff --git a/src/jobs/logout_user.rs b/src/jobs/logout_user.rs index c9b17d9e..575a55b0 100644 --- a/src/jobs/logout_user.rs +++ b/src/jobs/logout_user.rs @@ -1,5 +1,5 @@ use crate::database::repository::{auth_tokens, developers, refresh_tokens}; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use sqlx::PgConnection; pub async fn logout_user(username: &str, conn: &mut PgConnection) -> Result<(), ApiError> { diff --git a/src/jobs/token_cleanup.rs b/src/jobs/token_cleanup.rs index 00ef8b61..71ece5a9 100644 --- a/src/jobs/token_cleanup.rs +++ b/src/jobs/token_cleanup.rs @@ -1,5 +1,5 @@ use crate::database::repository::{auth_tokens, refresh_tokens}; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use sqlx::PgConnection; pub async fn token_cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { diff --git a/src/main.rs b/src/main.rs index 6dd6cb2e..01ce28b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,4 @@ -use crate::openapi::ApiDoc; -use crate::types::api; -use actix_cors::Cors; -use actix_web::{ - App, HttpServer, - web::{self, QueryConfig}, -}; -use utoipa::OpenApi; -use utoipa_swagger_ui::SwaggerUi; +use actix_web::web; mod abbreviate; mod auth; @@ -14,9 +6,9 @@ mod cli; mod config; mod database; mod dns; -mod endpoints; mod events; mod extractors; +mod http; mod jobs; mod logging; mod mod_zip; @@ -50,99 +42,8 @@ async fn main() -> anyhow::Result<()> { tracing::info!("Running migrations"); sqlx::migrate!("./migrations").run(app_data.db()).await?; - let port = app_data.port(); - let debug = app_data.debug(); - - let data = web::Data::new(app_data.clone()); + let data = web::Data::new(app_data); tokio::spawn(s3_worker::run_s3_worker(data.clone())); - tracing::info!("Starting server on 0.0.0.0:{}", port); - let server = HttpServer::new(move || { - let openapi = ApiDoc::openapi(); - - let app = App::new() - .app_data(data.clone()) - .app_data(QueryConfig::default().error_handler(api::query_error_handler)) - .service( - SwaggerUi::new("/swagger/{_:.*}").url("/swagger/openapi.json", openapi.clone()), - ) - .wrap( - Cors::default() - .allow_any_origin() - .allowed_methods(vec!["GET", "HEAD"]) - .allowed_headers(vec![actix_web::http::header::ACCEPT]) - .max_age(3600), - ) - .wrap(tracing_actix_web::TracingLogger::default()); - - #[cfg(feature = "serve-static")] - let app = app - .service(actix_files::Files::new("/static", "static")) - .service(actix_files::Files::new("/storage", "storage/public")); - - app.service(endpoints::mods::index) - .service(endpoints::mods::get_mod_updates) - .service(endpoints::mods::get) - .service(endpoints::mods::create) - .service(endpoints::mods::update_mod) - .service(endpoints::mods::get_logo) - .service(endpoints::mod_status_badge::status_badge) - .service(endpoints::mod_versions::get_version_index) - .service(endpoints::mod_versions::get_one) - .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::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) - .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) - .service(endpoints::deprecations::clear_all) - .service(endpoints::deprecations::delete) - .service(endpoints::auth::github::start_github_web_login) - .service(endpoints::auth::refresh_token) - .service(endpoints::auth::github::github_web_callback) - .service(endpoints::auth::github::poll_github_login) - .service(endpoints::auth::github::github_token_login) - .service(endpoints::auth::github::start_github_login) - .service(endpoints::developers::developer_index) - .service(endpoints::developers::get_developer) - .service(endpoints::developers::add_developer_to_mod) - .service(endpoints::developers::remove_dev_from_mod) - .service(endpoints::developers::delete_token) - .service(endpoints::developers::delete_tokens) - .service(endpoints::developers::update_profile) - .service(endpoints::developers::get_own_mods) - .service(endpoints::developers::get_me) - .service(endpoints::developers::update_developer) - .service(endpoints::developers::ban_developer) - .service(endpoints::developers::unban_developer) - .service(endpoints::developers::get_developer_ban) - .service(endpoints::tags::index) - .service(endpoints::tags::detailed_index) - .service(endpoints::stats::get_stats) - .service(endpoints::loader::get_one) - .service(endpoints::loader::create_version) - .service(endpoints::loader::get_many) - .service(endpoints::health::health) - }) - .bind(("0.0.0.0", port))?; - - if debug { - tracing::info!("Running in debug mode, using 1 thread."); - server.workers(1).run().await?; - } else { - server.run().await?; - } - - anyhow::Ok(()) + http::serve_http(data).await } diff --git a/src/openapi.rs b/src/openapi.rs index 18adb922..9e9d744c 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -1,7 +1,8 @@ use utoipa::OpenApi; -use crate::{endpoints, types}; +use crate::{http::endpoints, types}; +/// Routes served under the `/v1` scope. Their paths are relative to it, like the actix macros. #[derive(OpenApi)] #[openapi( paths( @@ -49,14 +50,20 @@ use crate::{endpoints, types}; endpoints::loader::get_one, endpoints::loader::create_version, endpoints::loader::get_many, - endpoints::health::health, endpoints::auth::refresh_token, endpoints::auth::github::start_github_login, endpoints::auth::github::start_github_web_login, endpoints::auth::github::github_web_callback, endpoints::auth::github::poll_github_login, endpoints::auth::github::github_token_login, - ), + ) +)] +struct V1Api; + +#[derive(OpenApi)] +#[openapi( + paths(endpoints::health::health), + nest((path = "/v1", api = V1Api)), components( schemas( types::api::ApiResponse, diff --git a/src/types/api.rs b/src/types/api.rs index 275f9e00..04e87e47 100644 --- a/src/types/api.rs +++ b/src/types/api.rs @@ -2,7 +2,7 @@ use actix_web::{HttpRequest, error::QueryPayloadError}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; #[derive(Serialize, Deserialize, Clone, ToSchema)] pub struct PaginatedData { diff --git a/src/types/mod_json.rs b/src/types/mod_json.rs index 234eeec1..da9bb457 100644 --- a/src/types/mod_json.rs +++ b/src/types/mod_json.rs @@ -8,7 +8,7 @@ use serde::Deserialize; use validator::{Validate, ValidationError}; use zip::read::ZipFile; -use crate::endpoints::format_validation_errors; +use crate::http::endpoints::format_validation_errors; use crate::mod_zip::{self, ModZipError}; use super::models::{ diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index 55421edb..61ff3ec3 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -9,10 +9,10 @@ use super::{ }; use crate::{ database::{DatabaseError, repository::developers}, - endpoints::ApiError, + http::endpoints::ApiError, }; use crate::{ - endpoints::{ + http::endpoints::{ developers::{SimpleDevMod, SimpleDevModVersion}, mods::{IndexQueryParams, IndexSortType}, }, diff --git a/src/types/models/stats.rs b/src/types/models/stats.rs index eef5793d..233d39ab 100644 --- a/src/types/models/stats.rs +++ b/src/types/models/stats.rs @@ -1,4 +1,4 @@ -use crate::{database::repository::developers, endpoints::ApiError}; +use crate::{database::repository::developers, http::endpoints::ApiError}; use chrono::Utc; use reqwest::{Client, header::HeaderValue}; use serde::{Deserialize, Serialize}; diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index 025ad2cc..604a0b54 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::database::{DatabaseError, repository::mod_tags}; -use crate::endpoints::ApiError; +use crate::http::endpoints::ApiError; use sqlx::PgConnection; use utoipa::ToSchema; From cd3ea6d6afa929bed412bf4f081ddc26e7ce1f80 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Thu, 22 Oct 2026 23:45:15 +0300 Subject: [PATCH 2/8] feat: rework cli module - remove cli::maybe_cli, replace with cli::run - route everything through cli::run - build app config after parsing args, so help can run without any deps --- Cargo.toml | 2 +- src/cli/jobs.rs | 37 ++++++++++++++++++++++++ src/cli/mod.rs | 70 +++++++++++---------------------------------- src/cli/serve.rs | 27 +++++++++++++++++ src/database/mod.rs | 16 +++++++++++ src/jobs/migrate.rs | 9 ------ src/main.rs | 22 ++------------ 7 files changed, 101 insertions(+), 82 deletions(-) create mode 100644 src/cli/jobs.rs create mode 100644 src/cli/serve.rs delete mode 100644 src/jobs/migrate.rs diff --git a/Cargo.toml b/Cargo.toml index 0e8d0ee2..74a6736a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ ammonia = "4" anyhow = "1.0" bytes = "1.11" chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4.5", features = ["derive", "env"] } dotenvy = "0.15" futures = "0.3" image = "0.25" diff --git a/src/cli/jobs.rs b/src/cli/jobs.rs new file mode 100644 index 00000000..7b75b0c0 --- /dev/null +++ b/src/cli/jobs.rs @@ -0,0 +1,37 @@ +use crate::{config::AppData, database, jobs}; + +#[derive(Debug, clap::Subcommand)] +pub enum JobCommand { + /// Cleans up mod_downloads from more than 30 days ago + CleanupDownloads, + /// Cleans up auth and refresh tokens that are expired + CleanupTokens, + /// Emergency logout for a developer + LogoutDeveloper { + /// Username of the developer + username: String, + }, + /// Runs migrations + Migrate, +} + +pub async fn run(job: JobCommand, data: AppData) -> anyhow::Result<()> { + let mut conn = data.db().acquire().await?; + + match job { + JobCommand::Migrate => { + database::migrate(&mut conn).await?; + } + JobCommand::CleanupDownloads => { + jobs::cleanup_downloads::cleanup_downloads(&mut conn).await?; + } + JobCommand::LogoutDeveloper { username } => { + jobs::logout_user::logout_user(&username, &mut conn).await?; + } + JobCommand::CleanupTokens => { + jobs::token_cleanup::token_cleanup(&mut conn).await?; + } + } + + Ok(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index deaa7c9e..89926e66 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,68 +1,32 @@ -use crate::config::AppData; -use crate::jobs; +use crate::{cli::serve::ServeArgs, config::AppData}; use clap::{Parser, Subcommand}; +mod jobs; +mod serve; + #[derive(Debug, Parser)] #[command(version, about, long_about = None)] -struct Args { +pub struct Args { #[command(subcommand)] command: Option, } #[derive(Subcommand, Debug)] -enum Commands { +pub enum Commands { + /// Run the HTTP server (default) + Serve(serve::ServeArgs), /// Run an internal job #[command(subcommand)] - Job(JobCommand), -} - -#[derive(Debug, Subcommand)] -enum JobCommand { - /// Cleans up mod_downloads from more than 30 days ago - CleanupDownloads, - /// Cleans up auth and refresh tokens that are expired - CleanupTokens, - /// Emergency logout for a developer - LogoutDeveloper { - /// Username of the developer - username: String, - }, - /// Runs migrations - Migrate, + Job(jobs::JobCommand), } -pub async fn maybe_cli(data: &AppData) -> anyhow::Result { - let cli = Args::parse(); - - if let Some(c) = cli.command { - return match c { - Commands::Job(job) => match job { - JobCommand::Migrate => { - let mut conn = data.db().acquire().await?; - jobs::migrate::migrate(&mut conn).await?; - - Ok(true) - } - JobCommand::CleanupDownloads => { - let mut conn = data.db().acquire().await?; - jobs::cleanup_downloads::cleanup_downloads(&mut conn).await?; - - Ok(true) - } - JobCommand::LogoutDeveloper { username } => { - let mut conn = data.db().acquire().await?; - jobs::logout_user::logout_user(&username, &mut conn).await?; - - Ok(true) - } - JobCommand::CleanupTokens => { - let mut conn = data.db().acquire().await?; - jobs::token_cleanup::token_cleanup(&mut conn).await?; - - Ok(true) - } - }, - }; +#[tracing::instrument(skip_all, fields(command = ?args.command))] +pub async fn run(args: Args, data: AppData) -> anyhow::Result<()> { + match args + .command + .unwrap_or_else(|| Commands::Serve(ServeArgs::default())) + { + Commands::Serve(serve_args) => serve::run(serve_args, data).await, + Commands::Job(job) => jobs::run(job, data).await, } - Ok(false) } diff --git a/src/cli/serve.rs b/src/cli/serve.rs new file mode 100644 index 00000000..6003d3f2 --- /dev/null +++ b/src/cli/serve.rs @@ -0,0 +1,27 @@ +use actix_web::web; + +use crate::{config::AppData, database, http, s3_worker}; + +#[derive(clap::Args, Debug, Default)] +pub struct ServeArgs { + /// Don't run database migrations on startup + #[arg(long, env = "SKIP_MIGRATIONS", value_parser = clap::builder::FalseyValueParser::new())] + skip_migrations: bool, +} + +pub async fn run(args: ServeArgs, data: AppData) -> anyhow::Result<()> { + data.static_storage().init().await?; + data.private_storage().init().await?; + + if let Some(cdn) = data.cdn_storage() { + cdn.init().await?; + } + + if !args.skip_migrations { + database::migrate(data.db()).await?; + } + + let data = web::Data::new(data); + tokio::spawn(s3_worker::run_s3_worker(data.clone())); + http::serve_http(data).await +} diff --git a/src/database/mod.rs b/src/database/mod.rs index b19a744b..15222f5f 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,3 +1,7 @@ +use std::ops::Deref; + +use sqlx::{Acquire, migrate::Migrate}; + pub mod repository; #[derive(thiserror::Error, Debug)] @@ -7,3 +11,15 @@ pub enum DatabaseError { #[error("Unknown database error")] SqlxError(#[from] sqlx::Error), } + +pub async fn migrate<'a, A>(conn: A) -> anyhow::Result<()> +where + A: Acquire<'a>, + ::Target: Migrate, +{ + sqlx::migrate!() + .run(conn) + .await + .inspect_err(|e| tracing::error!("Failed to run migrations: {e}"))?; + Ok(()) +} diff --git a/src/jobs/migrate.rs b/src/jobs/migrate.rs deleted file mode 100644 index b65dd793..00000000 --- a/src/jobs/migrate.rs +++ /dev/null @@ -1,9 +0,0 @@ -use sqlx::PgConnection; - -pub async fn migrate(pool: &mut PgConnection) -> anyhow::Result<()> { - if let Err(e) = sqlx::migrate!("./migrations").run(&mut *pool).await { - tracing::error!("Error encountered while running migrations: {}", e); - } - - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index 01ce28b9..2d5ab932 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use actix_web::web; +use clap::Parser; mod abbreviate; mod auth; @@ -26,24 +26,8 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; #[tokio::main] async fn main() -> anyhow::Result<()> { + let args = cli::Args::parse(); logging::init(); - let app_data = config::build_config().await?; - app_data.static_storage().init().await?; - app_data.private_storage().init().await?; - if let Some(cdn_storage) = app_data.cdn_storage() { - cdn_storage.init().await?; - } - - if cli::maybe_cli(&app_data).await? { - return Ok(()); - } - - tracing::info!("Running migrations"); - sqlx::migrate!("./migrations").run(app_data.db()).await?; - - let data = web::Data::new(app_data); - tokio::spawn(s3_worker::run_s3_worker(data.clone())); - - http::serve_http(data).await + cli::run(args, app_data).await } From 7647f019e6a8a88f421e4a6799c32a7a04f1d55f Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 23 Oct 2026 00:30:16 +0300 Subject: [PATCH 3/8] fix: oops --- src/jobs/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index f359b066..1e95982d 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -1,4 +1,3 @@ pub mod cleanup_downloads; pub mod logout_user; -pub mod migrate; pub mod token_cleanup; From e32faf92ba38841809d53426964d144c27f23884 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 23 Oct 2026 00:30:30 +0300 Subject: [PATCH 4/8] feat: improve s3_worker logging --- src/cli/serve.rs | 5 +- src/s3_worker.rs | 264 +++++++++++++++++++++++++++++------------------ 2 files changed, 167 insertions(+), 102 deletions(-) diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 6003d3f2..ee11dcdc 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,4 +1,5 @@ use actix_web::web; +use tracing::Instrument; use crate::{config::AppData, database, http, s3_worker}; @@ -22,6 +23,8 @@ pub async fn run(args: ServeArgs, data: AppData) -> anyhow::Result<()> { } let data = web::Data::new(data); - tokio::spawn(s3_worker::run_s3_worker(data.clone())); + tokio::spawn( + s3_worker::run_s3_worker(data.clone()).instrument(tracing::info_span!("s3_worker")), + ); http::serve_http(data).await } diff --git a/src/s3_worker.rs b/src/s3_worker.rs index e39a1c3d..8329116e 100644 --- a/src/s3_worker.rs +++ b/src/s3_worker.rs @@ -219,19 +219,32 @@ async fn upload_mod_logo( update_mod_logo_url(mod_id, &logo_public_url, db).await?; - tracing::info!("Uploaded logo for {} to S3 at {}", mod_id, logo_public_url); + tracing::info!(url = %logo_public_url, "uploaded mod logo"); } Ok(()) } +#[tracing::instrument( + name = "s3_task", + skip_all, + fields( + is_migration = is_migration, + kind = tracing::field::Empty, + mod_id = tracing::field::Empty, + version = tracing::field::Empty, + version_id = tracing::field::Empty, + size = tracing::field::Empty, + tag = tracing::field::Empty, + ), + err(Debug) +)] async fn process_task( data: &AppData, task: S3WorkerTask, is_migration: bool, ) -> anyhow::Result<()> { - let storage = data.cdn_storage().expect("mod storage must be set by now"); - let mut db = data.db().acquire().await?; + let span = tracing::Span::current(); match task { S3WorkerTask::UploadMod { @@ -240,57 +253,83 @@ async fn process_task( version, version_id, } => { - let path = path_for_mod(&mod_id, &version); - let public_url = storage.asset_url(&path); - - storage - .store(&path, &bytes, "application/octet-stream") - .await?; - - update_managed_download_link(version_id, Some(&public_url), &mut db).await?; + span.record("kind", "upload_mod") + .record("mod_id", mod_id.as_str()) + .record("version", version.as_str()) + .record("version_id", version_id) + .record("size", bytes.len()); - // upload logo if not migrating mods - if !is_migration { - upload_mod_logo(data, &mod_id, None, &mut db).await?; - } - - tracing::info!( - "Uploaded mod {} {} to S3 at {}", - mod_id, - version, - public_url - ); + upload_mod(data, bytes, &mod_id, &version, version_id, is_migration).await } S3WorkerTask::UploadLoader { tag } => { - tracing::info!("Preparing to upload Geode v{tag} to S3"); + span.record("kind", "upload_loader") + .record("tag", tag.as_str()); - let mut tx = data.db().begin().await?; + upload_loader(data, &tag).await + } + } +} - let downloads = LoaderDownloads { - win: migrate_geode_version(data, &mut tx, &tag, "win").await?, - mac: migrate_geode_version(data, &mut tx, &tag, "mac").await?, - android32: migrate_geode_version(data, &mut tx, &tag, "android32").await?, - android64: migrate_geode_version(data, &mut tx, &tag, "android64").await?, - ios: migrate_artifact(data, &mut tx, &LoaderMigration::new(&tag, "ios")) - .await? - .unwrap_or_default(), - resources: migrate_resources(data, &mut tx, &tag).await?, +async fn upload_mod( + data: &AppData, + bytes: Bytes, + mod_id: &str, + version: &str, + version_id: i32, + is_migration: bool, +) -> anyhow::Result<()> { + let storage = data.cdn_storage().expect("mod storage must be set by now"); + let mut db = data.db().acquire().await?; - win_installer: migrate_geode_installer(data, &mut tx, &tag, "win").await?, - mac_installer: migrate_geode_installer(data, &mut tx, &tag, "mac").await?, - linux_installer: migrate_geode_installer(data, &mut tx, &tag, "linux").await?, - }; + let path = path_for_mod(mod_id, version); + let public_url = storage.asset_url(&path); - tx.commit().await?; + storage + .store(&path, &bytes, "application/octet-stream") + .await?; - tracing::info!("Uploaded new loader release to S3: {downloads:?}"); - } + update_managed_download_link(version_id, Some(&public_url), &mut db).await?; + + // upload logo if not migrating mods + if !is_migration { + upload_mod_logo(data, mod_id, None, &mut db).await?; } + tracing::info!(url = %public_url, "uploaded mod"); + + Ok(()) +} + +async fn upload_loader(data: &AppData, tag: &str) -> anyhow::Result<()> { + tracing::info!("uploading loader release"); + + let mut tx = data.db().begin().await?; + + let downloads = LoaderDownloads { + win: migrate_geode_version(data, &mut tx, tag, "win").await?, + mac: migrate_geode_version(data, &mut tx, tag, "mac").await?, + android32: migrate_geode_version(data, &mut tx, tag, "android32").await?, + android64: migrate_geode_version(data, &mut tx, tag, "android64").await?, + ios: migrate_artifact(data, &mut tx, &LoaderMigration::new(tag, "ios")) + .await? + .unwrap_or_default(), + resources: migrate_resources(data, &mut tx, tag).await?, + + win_installer: migrate_geode_installer(data, &mut tx, tag, "win").await?, + mac_installer: migrate_geode_installer(data, &mut tx, tag, "mac").await?, + linux_installer: migrate_geode_installer(data, &mut tx, tag, "linux").await?, + }; + + tx.commit().await?; + + tracing::info!("uploaded loader release"); + tracing::debug!(?downloads, "loader release downloads"); + Ok(()) } +#[tracing::instrument(name = "s3_cleanup", skip_all, err(Debug))] async fn cleanup_old_s3_files(data: &AppData) -> anyhow::Result<()> { let supported_gd = GDVersionEnum::supported_for_storage(); let storage = data.cdn_storage().expect("mod storage must be set by now"); @@ -316,21 +355,20 @@ async fn cleanup_old_s3_files(data: &AppData) -> anyhow::Result<()> { .fetch_all(&mut *data.db().acquire().await?) .await?; - tracing::info!("Cleaning up {} old S3 files", versions.len()); + if versions.is_empty() { + return Ok(()); + } + + tracing::info!(total = versions.len(), "cleaning up old S3 files"); for record in versions { let path = path_for_mod(&record.mod_id, &record.version); if let Err(e) = storage.delete(&path).await { - tracing::error!( - "error deleting old S3 file for mod {} {} at {:?}: {e:?}", - record.mod_id, - record.version, - record.managed_download_link - ); + tracing::error!(error = ?e, mod_id = %record.mod_id, version = %record.version, %path, "failed to delete old S3 file"); continue; } - tracing::debug!("Deleted S3 file at path {}", path); + tracing::debug!(%path, "deleted old S3 file"); let mut tx = data.db().begin().await?; update_managed_download_link(record.id, None, &mut tx).await?; @@ -340,6 +378,12 @@ async fn cleanup_old_s3_files(data: &AppData) -> anyhow::Result<()> { Ok(()) } +#[tracing::instrument( + name = "s3_migrate_mod", + skip_all, + fields(mod_id = %mod_id, version = %version, version_id = version_id), + err(Debug) +)] async fn migrate_one( data: &AppData, original_url: &str, @@ -354,19 +398,20 @@ async fn migrate_one( ) .await?; - process_task( - data, - S3WorkerTask::UploadMod { - data: bytes, - mod_id: mod_id.to_owned(), - version: version.to_owned(), - version_id, - }, - true, - ) - .await + upload_mod(data, bytes, mod_id, version, version_id, true).await +} + +#[tracing::instrument(name = "s3_migrate_logo", skip_all, fields(mod_id = %mod_id), err(Debug))] +async fn migrate_mod_logo( + data: &AppData, + mod_id: &str, + image: Option>, + db: &mut PgConnection, +) -> anyhow::Result<()> { + upload_mod_logo(data, mod_id, image, db).await } +#[tracing::instrument(name = "s3_migrate_mods", skip_all, err(Debug))] async fn migrate_existing_mods_to_s3(data: &AppData) -> anyhow::Result<()> { let supported_gd = GDVersionEnum::supported_for_storage(); @@ -390,24 +435,28 @@ async fn migrate_existing_mods_to_s3(data: &AppData) -> anyhow::Result<()> { .fetch_all(&mut *data.db().acquire().await?) .await?; - tracing::info!("Migrating {} existing mods to S3", versions.len()); + if !versions.is_empty() { + let total = versions.len(); + let mut failed = 0; - for record in versions { - if let Err(e) = migrate_one( - data, - &record.download_link, - &record.id, - &record.version, - record.mv_id, - ) - .await - { - tracing::error!( - "error migrating mod {} {} to S3: {e:?}", - record.id, - record.version - ); + tracing::info!(total = versions.len(), "migrating existing mods to S3"); + + for record in versions { + if migrate_one( + data, + &record.download_link, + &record.id, + &record.version, + record.mv_id, + ) + .await + .is_err() + { + failed += 1; + } } + + tracing::info!(total, failed, "finished migrating mods"); } // independently migrate mod logos @@ -418,17 +467,28 @@ async fn migrate_existing_mods_to_s3(data: &AppData) -> anyhow::Result<()> { .fetch_all(&mut *db) .await?; - tracing::info!("Migrating {} existing mod logos to S3", mods.len()); + if !mods.is_empty() { + let total = mods.len(); + let mut failed = 0; - for record in mods { - if let Err(e) = upload_mod_logo(data, &record.id, record.image, &mut db).await { - tracing::error!("error migrating mod logo for {} to S3: {e:?}", record.id); + tracing::info!(total = mods.len(), "migrating mod logos to S3"); + + for record in mods { + if migrate_mod_logo(data, &record.id, record.image, &mut db) + .await + .is_err() + { + failed += 1; + } } + + tracing::info!(total, failed, "finished migrating mod logos"); } Ok(()) } +#[tracing::instrument(name = "s3_migrate_loader", skip_all, err(Debug))] async fn migrate_loader_versions_to_s3(data: &AppData) -> anyhow::Result<()> { let versions: Vec = sqlx::query_scalar( "SELECT gv.tag FROM geode_versions gv WHERE NOT EXISTS ( @@ -439,14 +499,22 @@ async fn migrate_loader_versions_to_s3(data: &AppData) -> anyhow::Result<()> { .await .map_err(|e| anyhow::anyhow!("{e}"))?; - tracing::info!("Migrating {} Geode releases to S3", versions.len()); + if !versions.is_empty() { + let total = versions.len(); + let mut failed = 0; + + tracing::info!(total = versions.len(), "migrating Geode releases"); - for tag in versions { - if let Err(e) = - process_task(data, S3WorkerTask::UploadLoader { tag: tag.clone() }, true).await - { - tracing::error!("error migrating Geode release {} to S3: {e:?}", tag); + for tag in versions { + if process_task(data, S3WorkerTask::UploadLoader { tag }, true) + .await + .is_err() + { + failed += 1; + } } + + tracing::info!(total, failed, "finished migrating Geode releases"); } Ok(()) @@ -462,32 +530,26 @@ pub async fn run_s3_worker(data: web::Data) { let s_data = data.clone(); tokio::spawn(async move { - if let Err(e) = migrate_existing_mods_to_s3(&s_data).await { - tracing::error!("Error migrating existing mods to S3: {:?}", e); - } + let _ = migrate_existing_mods_to_s3(&s_data).await; }); let s_data2 = data.clone(); tokio::spawn(async move { - if let Err(e) = migrate_loader_versions_to_s3(&s_data2).await { - tracing::error!("Error migrating loader versions to S3: {:?}", e); - } + let _ = migrate_loader_versions_to_s3(&s_data2).await; }); let mut interval = tokio::time::interval(Duration::from_mins(30)); loop { - let result = tokio::select! { - task = rx.recv() => match task { - Some(task) => process_task(&data, task, false).await, - None => break, - }, + tokio::select! { + task = rx.recv() => { + let Some(task) = task else { break }; + let _ = process_task(&data, task, false).await; + } - _ = interval.tick() => cleanup_old_s3_files(&data).await, + _ = interval.tick() => { + let _ = cleanup_old_s3_files(&data).await; + }, }; - - if let Err(e) = result { - tracing::error!("Error processing S3 worker task: {:?}", e); - } } } From 89fb68e0fcf66ef04e943e29ab991f6ef7c05fb0 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 23 Oct 2026 00:44:39 +0300 Subject: [PATCH 5/8] feat: link request id to s3_worker tasks --- src/config.rs | 18 +++++++++--------- src/http/mod.rs | 20 ++++++++++++++++---- src/s3_worker.rs | 16 ++++++++++++---- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/config.rs b/src/config.rs index 34d02ecf..2a3b4821 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,8 +9,8 @@ use tokio::sync::mpsc::Sender; use crate::{ auth::github::GithubClient, dns::ValidateDnsResolver, - http::endpoints::mods::IndexQueryParams, - s3_worker::S3WorkerTask, + http::{self, endpoints::mods::IndexQueryParams}, + s3_worker::{QueuedS3Task, S3WorkerTask}, storage::{LocalBackend, PrivateDisk, PublicDisk, S3Backend, S3Configuration}, types::{ api::{ApiResponse, PaginatedData}, @@ -39,7 +39,7 @@ pub struct AppData { http_client: reqwest::Client, check_dns_http_client: reqwest::Client, - s3_sender: OnceLock>, + s3_sender: OnceLock>, } pub async fn build_config() -> anyhow::Result { @@ -198,17 +198,17 @@ impl AppData { &self.check_dns_http_client } - pub fn init_s3_sender(&self, sender: Sender) { + pub fn init_s3_sender(&self, sender: Sender) { self.s3_sender .set(sender) .expect("init_s3_sender must be called only once"); } pub fn send_s3_task(&self, task: S3WorkerTask) -> bool { - if let Some(sender) = self.s3_sender.get() { - sender.try_send(task).is_ok() - } else { - false - } + let Some(sender) = self.s3_sender.get() else { + return false; + }; + let request_id = http::REQUEST_ID.try_with(|id| *id).ok(); + sender.try_send(QueuedS3Task { task, request_id }).is_ok() } } diff --git a/src/http/mod.rs b/src/http/mod.rs index 64c3f785..27781d35 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,10 +1,8 @@ use actix_cors::Cors; use actix_web::{ - App, HttpServer, - body::MessageBody, - dev::{ServiceFactory, ServiceRequest, ServiceResponse}, - web::{self, QueryConfig}, + App, HttpMessage, HttpServer, body::MessageBody, dev::{Service, ServiceFactory, ServiceRequest, ServiceResponse}, web::{self, QueryConfig}, }; +use tracing_actix_web::RequestId; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -20,6 +18,10 @@ use crate::{ pub mod endpoints; +tokio::task_local! { + pub static REQUEST_ID: RequestId; +} + fn router_config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/v1") @@ -62,6 +64,16 @@ pub fn create_app( .app_data(QueryConfig::default().error_handler(api::query_error_handler)) .service(SwaggerUi::new("/swagger/{_:.*}").url("/swagger/openapi.json", openapi.clone())) .wrap(cors_settings()) + .wrap_fn(|req, srv| { + let request_id = req.extensions().get::().copied(); + let fut = srv.call(req); + async move { + match request_id { + Some(id) => REQUEST_ID.scope(id, fut).await, + None => fut.await + } + } + }) .wrap(tracing_actix_web::TracingLogger::default()); #[cfg(feature = "serve-static")] diff --git a/src/s3_worker.rs b/src/s3_worker.rs index 8329116e..807391e5 100644 --- a/src/s3_worker.rs +++ b/src/s3_worker.rs @@ -4,6 +4,7 @@ use actix_web::web; use bytes::Bytes; use reqwest::StatusCode; use sqlx::PgConnection; +use tracing_actix_web::RequestId; use crate::{ config::AppData, @@ -19,6 +20,11 @@ use crate::{ }, }; +pub(crate) struct QueuedS3Task { + pub task: S3WorkerTask, + pub request_id: Option, +} + pub enum S3WorkerTask { UploadMod { data: Bytes, @@ -229,6 +235,7 @@ async fn upload_mod_logo( name = "s3_task", skip_all, fields( + request_id = request_id.map(tracing::field::display), is_migration = is_migration, kind = tracing::field::Empty, mod_id = tracing::field::Empty, @@ -243,6 +250,7 @@ async fn process_task( data: &AppData, task: S3WorkerTask, is_migration: bool, + request_id: Option, ) -> anyhow::Result<()> { let span = tracing::Span::current(); @@ -506,7 +514,7 @@ async fn migrate_loader_versions_to_s3(data: &AppData) -> anyhow::Result<()> { tracing::info!(total = versions.len(), "migrating Geode releases"); for tag in versions { - if process_task(data, S3WorkerTask::UploadLoader { tag }, true) + if process_task(data, S3WorkerTask::UploadLoader { tag }, true, None) .await .is_err() { @@ -542,9 +550,9 @@ pub async fn run_s3_worker(data: web::Data) { loop { tokio::select! { - task = rx.recv() => { - let Some(task) = task else { break }; - let _ = process_task(&data, task, false).await; + queued = rx.recv() => { + let Some(queued) = queued else { break }; + let _ = process_task(&data, queued.task, false, queued.request_id).await; } _ = interval.tick() => { From a5c78afb1df33047804a3f4c10b4c996ee8073e0 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 23 Oct 2026 00:52:14 +0300 Subject: [PATCH 6/8] fix: remove version from openapi.rs openapi will be generated with the crate version now --- src/openapi.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/openapi.rs b/src/openapi.rs index 9e9d744c..2bbf7fe6 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -116,7 +116,6 @@ struct V1Api; ), info( title = "Geode Index API", - version = "0.53.2", description = "API for the Geode mod index", contact( name = "Geode Team", From cc0f7f5a292981dbfe235ee6e4d4d33357a4cf9d Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 23 Oct 2026 00:58:59 +0300 Subject: [PATCH 7/8] fix: move around some more stuff --- src/config.rs | 6 +- src/http/endpoints/auth/github.rs | 4 +- src/http/endpoints/auth/mod.rs | 6 +- src/http/endpoints/deprecations.rs | 6 +- src/http/endpoints/developers.rs | 32 +--- src/http/endpoints/loader.rs | 6 +- src/http/endpoints/mod.rs | 158 ---------------- src/http/endpoints/mod_status_badge.rs | 2 +- src/http/endpoints/mod_version_submissions.rs | 7 +- src/http/endpoints/mod_versions.rs | 5 +- src/http/endpoints/mods.rs | 41 +--- src/http/endpoints/stats.rs | 5 +- src/http/endpoints/tags.rs | 4 +- src/http/error.rs | 178 ++++++++++++++++++ src/{ => http}/extractors/auth.rs | 2 +- src/{ => http}/extractors/mod.rs | 0 src/http/mod.rs | 17 +- src/{ => http}/openapi.rs | 10 +- src/http/response.rs | 8 + src/jobs/cleanup_downloads.rs | 2 +- src/jobs/logout_user.rs | 2 +- src/jobs/token_cleanup.rs | 2 +- src/main.rs | 2 - src/mod_zip.rs | 20 +- src/types/api.rs | 13 -- src/types/mod_json.rs | 2 +- src/types/models/mod_entity.rs | 61 +++++- src/types/models/stats.rs | 2 +- src/types/models/tag.rs | 2 +- 29 files changed, 303 insertions(+), 302 deletions(-) create mode 100644 src/http/error.rs rename src/{ => http}/extractors/auth.rs (98%) rename src/{ => http}/extractors/mod.rs (100%) rename src/{ => http}/openapi.rs (95%) create mode 100644 src/http/response.rs diff --git a/src/config.rs b/src/config.rs index 2a3b4821..1e12d8f8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,12 +9,12 @@ use tokio::sync::mpsc::Sender; use crate::{ auth::github::GithubClient, dns::ValidateDnsResolver, - http::{self, endpoints::mods::IndexQueryParams}, + http::{self, response::ApiResponse}, s3_worker::{QueuedS3Task, S3WorkerTask}, storage::{LocalBackend, PrivateDisk, PublicDisk, S3Backend, S3Configuration}, types::{ - api::{ApiResponse, PaginatedData}, - models::mod_entity::Mod, + api::PaginatedData, + models::mod_entity::{IndexQueryParams, Mod}, }, }; diff --git a/src/http/endpoints/auth/github.rs b/src/http/endpoints/auth/github.rs index fa5b30e3..f7b6d3b6 100644 --- a/src/http/endpoints/auth/github.rs +++ b/src/http/endpoints/auth/github.rs @@ -9,9 +9,9 @@ use crate::config::AppData; use crate::database::repository::{ auth_tokens, developers, github_login_attempts, github_web_logins, refresh_tokens, }; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use crate::http::endpoints::auth::TokensResponse; -use crate::types::api::ApiResponse; +use crate::http::response::ApiResponse; #[derive(Deserialize, ToSchema)] struct PollParams { diff --git a/src/http/endpoints/auth/mod.rs b/src/http/endpoints/auth/mod.rs index c6028a59..688693da 100644 --- a/src/http/endpoints/auth/mod.rs +++ b/src/http/endpoints/auth/mod.rs @@ -1,8 +1,8 @@ use crate::config::AppData; use crate::database::repository::{auth_tokens, developers, refresh_tokens}; -use crate::http::endpoints::ApiError; -use crate::extractors::auth::Auth; -use crate::types::api::ApiResponse; +use crate::http::error::ApiError; +use crate::http::extractors::auth::Auth; +use crate::http::response::ApiResponse; use actix_web::{Responder, post, web}; use serde::{Deserialize, Serialize}; use sqlx::Acquire; diff --git a/src/http/endpoints/deprecations.rs b/src/http/endpoints/deprecations.rs index c9bb8e48..cb75026b 100644 --- a/src/http/endpoints/deprecations.rs +++ b/src/http/endpoints/deprecations.rs @@ -1,9 +1,9 @@ use crate::{ config::AppData, database::repository::{deprecations, developers, mods}, - http::endpoints::ApiError, - extractors::auth::Auth, - types::api::ApiResponse, + http::error::ApiError, + http::extractors::auth::Auth, + http::response::ApiResponse, types::models::deprecations::Deprecation, }; use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; diff --git a/src/http/endpoints/developers.rs b/src/http/endpoints/developers.rs index c5d8b7a1..360be6e3 100644 --- a/src/http/endpoints/developers.rs +++ b/src/http/endpoints/developers.rs @@ -1,42 +1,24 @@ use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use sqlx::Acquire; use utoipa::{IntoParams, ToSchema}; use chrono::{DateTime, Utc}; -use super::ApiError; +use crate::http::error::ApiError; use crate::config::AppData; use crate::database::repository::{auth_tokens, developers, mods, refresh_tokens}; -use crate::types::api::{ApiResponse, PaginatedData}; +use crate::http::response::ApiResponse; +use crate::types::api::PaginatedData; use crate::types::models::developer::SelfDeveloper; use crate::{ - extractors::auth::Auth, + http::extractors::auth::Auth, types::models::{ - developer::{Developer, ModDeveloper, DeveloperBan}, - mod_entity::Mod, + developer::{Developer, DeveloperBan}, + mod_entity::{Mod, SimpleDevMod}, mod_version_status::ModVersionStatusEnum, }, }; -#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)] -pub struct SimpleDevMod { - pub id: String, - pub featured: bool, - pub download_count: i32, - pub versions: Vec, - pub developers: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)] -pub struct SimpleDevModVersion { - pub name: String, - pub version: String, - pub download_count: i32, - pub validated: bool, - pub info: Option, - pub status: ModVersionStatusEnum, -} - #[derive(Deserialize, IntoParams)] struct AddDevPath { id: String, diff --git a/src/http/endpoints/loader.rs b/src/http/endpoints/loader.rs index 06fbc448..9d1a9d9f 100644 --- a/src/http/endpoints/loader.rs +++ b/src/http/endpoints/loader.rs @@ -5,13 +5,13 @@ use utoipa::{IntoParams, ToSchema}; use sqlx::Acquire; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use crate::s3_worker::S3WorkerTask; use crate::{ config::AppData, - extractors::auth::Auth, + http::{extractors::auth::Auth, response::ApiResponse}, types::{ - api::{ApiResponse, PaginatedData}, + api::PaginatedData, models::{ gd_version_alias::GDVersionAlias, loader_version::{GetVersionsQuery, LoaderVersion, LoaderVersionCreate}, diff --git a/src/http/endpoints/mod.rs b/src/http/endpoints/mod.rs index a671df77..df58b82a 100644 --- a/src/http/endpoints/mod.rs +++ b/src/http/endpoints/mod.rs @@ -1,10 +1,3 @@ -use crate::{ - mod_zip::ModZipError, - types::{api::ApiResponse, models::mod_gd_version::PlatformParseError}, -}; -use actix_web::{HttpResponse, http::StatusCode}; -use validator::{ValidationError, ValidationErrors}; - pub mod auth; pub mod deprecations; pub mod developers; @@ -16,154 +9,3 @@ pub mod mod_versions; pub mod mods; pub mod stats; pub mod tags; - -#[derive(thiserror::Error, Debug)] -pub enum ApiError { - #[error("Authentication error: {0}")] - Authentication(#[from] crate::auth::AuthenticationError), - #[error("You do not have access to this resource")] - Authorization, - #[error("{0}")] - Database(#[from] crate::database::DatabaseError), - #[error("{0}")] - Storage(#[from] crate::storage::StorageError), - #[error("{0}")] - ModZip(#[from] ModZipError), - #[error("Database error")] - SqlxError(#[from] sqlx::Error), - #[error("Failed to parse response data: {0}")] - Json(#[from] serde_json::Error), - #[error("{0}")] - BadRequest(String), - #[error("{0}")] - #[allow(dead_code)] - TooManyRequests(String), - #[error("Internal error: {0}")] - InternalError(String), - #[error("{0}")] - NotFound(String), - #[error("Error: {0}")] - PlatformParseError(#[from] PlatformParseError), - #[error("Unable to unzip archive")] - 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), - #[error("You are banned from accessing this resource: {}", .0.as_deref().unwrap_or("No reason provided"))] - Banned(Option), -} - -impl ApiError { - pub fn as_response(&self) -> ApiResponse { - ApiResponse { - error: self.to_string(), - payload: "".into(), - } - } -} - -impl actix_web::ResponseError for ApiError { - fn status_code(&self) -> StatusCode { - match self { - ApiError::Authentication(..) => StatusCode::UNAUTHORIZED, - ApiError::Authorization => StatusCode::FORBIDDEN, - ApiError::Json(..) => StatusCode::BAD_REQUEST, - ApiError::TooManyRequests(..) => StatusCode::TOO_MANY_REQUESTS, - ApiError::NotFound(_) => StatusCode::NOT_FOUND, - ApiError::BadRequest(..) => StatusCode::BAD_REQUEST, - ApiError::ModZip(err) => err.status_code(), - ApiError::PlatformParseError(_) => StatusCode::BAD_REQUEST, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).json(self.as_response()) - } -} - -// validator errors - -impl From for ApiError { - fn from(value: ValidationError) -> Self { - ApiError::BadRequest(format_validation_error(&value)) - } -} - -impl From for ApiError { - fn from(value: ValidationErrors) -> Self { - ApiError::BadRequest(format_validation_errors(&value)) - } -} - -pub fn format_validation_error(e: &validator::ValidationError) -> String { - if let Some(msg) = &e.message { - return msg.to_string(); - } - - match e.code.as_ref() { - "length" => { - let min = e - .params - .get("min") - .and_then(|v| v.as_u64()) - .map(|v| v.to_string()); - - let max = e - .params - .get("max") - .and_then(|v| v.as_u64()) - .map(|v| v.to_string()); - - match (min, max) { - (Some(min), Some(max)) => { - format!("length must be between {min} and {max} characters") - } - (Some(min), None) => format!("length must be at least {min} characters"), - (None, Some(max)) => format!("length must be at most {max} characters"), - (None, None) => "invalid length".to_string(), - } - } - - e => e.to_owned(), - } -} - -pub fn format_validation_errors(e: &validator::ValidationErrors) -> String { - use validator::ValidationErrorsKind; - - let mut str_errors = Vec::new(); - - for (field, err) in e.errors() { - match err { - ValidationErrorsKind::Struct(s) => { - str_errors.push(format!( - "field '{field}' is invalid ({})", - format_validation_errors(s) - )); - } - - ValidationErrorsKind::Field(errors) => { - let joined = errors - .iter() - .map(format_validation_error) - .collect::>() - .join(", "); - - str_errors.push(format!("field '{field}' is invalid: {}", joined)); - } - - ValidationErrorsKind::List(map) => { - for (index, errors) in map { - str_errors.push(format!( - "field '{field}' at index {index} is invalid ({})", - format_validation_errors(errors) - )); - } - } - } - } - - str_errors.join(", ") -} diff --git a/src/http/endpoints/mod_status_badge.rs b/src/http/endpoints/mod_status_badge.rs index cd7f7095..54e008ec 100644 --- a/src/http/endpoints/mod_status_badge.rs +++ b/src/http/endpoints/mod_status_badge.rs @@ -1,5 +1,5 @@ use crate::config::AppData; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use actix_web::{HttpResponse, Responder, get, web}; use serde::Deserialize; use utoipa::{IntoParams, ToSchema}; diff --git a/src/http/endpoints/mod_version_submissions.rs b/src/http/endpoints/mod_version_submissions.rs index f178caaa..73f3e3f1 100644 --- a/src/http/endpoints/mod_version_submissions.rs +++ b/src/http/endpoints/mod_version_submissions.rs @@ -1,10 +1,11 @@ -use super::ApiError; +use crate::http::error::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::types::api::{ApiResponse, PaginatedData}; +use crate::http::extractors::auth::Auth; +use crate::http::response::ApiResponse; +use crate::types::api::PaginatedData; use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::developer::Developer; use crate::types::models::mod_version_status::ModVersionStatusEnum; diff --git a/src/http/endpoints/mod_versions.rs b/src/http/endpoints/mod_versions.rs index a3f378fb..f2b67fce 100644 --- a/src/http/endpoints/mod_versions.rs +++ b/src/http/endpoints/mod_versions.rs @@ -11,7 +11,7 @@ use crate::database::repository::{ dependencies, developers, incompatibilities, mod_downloads, mod_gd_versions, mod_links, mod_tags, mod_version_submissions, mod_versions, mods, }; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use crate::events::mod_created::{ NewModAcceptedEvent, NewModVersionAcceptedEvent, NewModVersionVerification, NewUnverifiedModVersionCreated, @@ -22,9 +22,8 @@ use crate::types::models; use crate::types::models::mod_version_submission::ModVersionSubmissionLock; use crate::webhook::discord::DiscordWebhook; use crate::{ - extractors::auth::Auth, + http::{extractors::auth::Auth, response::ApiResponse}, types::{ - api::ApiResponse, mod_json::{ModJson, split_version_and_compare}, models::{ mod_gd_version::{GDVersionEnum, VerPlatform}, diff --git a/src/http/endpoints/mods.rs b/src/http/endpoints/mods.rs index c407494e..127ebce2 100644 --- a/src/http/endpoints/mods.rs +++ b/src/http/endpoints/mods.rs @@ -12,16 +12,17 @@ use crate::database::repository::mod_versions; use crate::database::repository::mods; use crate::database::repository::mods::ModLogo; use crate::database::repository::{dependencies, deprecations, mod_version_submissions}; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use crate::events::mod_created::NewUnverifiedModVersionCreated; use crate::events::mod_feature::ModFeaturedEvent; -use crate::extractors::auth::Auth; +use crate::http::extractors::auth::Auth; use crate::mod_zip; -use crate::types::api::{ApiResponse, PaginatedData}; +use crate::http::response::ApiResponse; +use crate::types::api::PaginatedData; use crate::types::mod_json::ModJson; use crate::types::models; use crate::types::models::deprecations::Deprecation; -use crate::types::models::mod_entity::{Mod, ModUpdate}; +use crate::types::models::mod_entity::{IndexQueryParams, Mod, ModUpdate}; use crate::types::models::mod_gd_version::{GDVersionEnum, VerPlatform}; use crate::types::models::mod_link::ModLinks; use crate::types::models::mod_version_status::ModVersionStatusEnum; @@ -35,38 +36,6 @@ use validator::Validate; const MAX_UPDATE_BATCH_SIZE: usize = 200; -#[derive(Deserialize, Default, Hash, Eq, PartialEq, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum IndexSortType { - #[default] - Downloads, - RecentlyUpdated, - RecentlyPublished, - Oldest, - Name, - NameReverse, - Random, -} - -#[derive(Deserialize, Hash, Eq, PartialEq, IntoParams)] -pub struct IndexQueryParams { - pub page: Option, - pub per_page: Option, - pub query: Option, - #[serde(default)] - pub gd: Option, - #[serde(default)] - pub platforms: Option, - #[serde(default)] - pub sort: IndexSortType, - pub geode: Option, - pub developer: Option, - pub tags: Option, - pub featured: Option, - pub jitless: Option, - pub status: Option, -} - #[derive(Deserialize, ToSchema, Validate)] pub struct CreateQueryParams { #[validate(length(max = 1024))] diff --git a/src/http/endpoints/stats.rs b/src/http/endpoints/stats.rs index 5b2afffc..726320d5 100644 --- a/src/http/endpoints/stats.rs +++ b/src/http/endpoints/stats.rs @@ -1,8 +1,9 @@ use actix_web::{Responder, get, web}; -use super::ApiError; +use crate::http::error::ApiError; use crate::config::AppData; -use crate::types::{api::ApiResponse, models::stats::Stats}; +use crate::http::response::ApiResponse; +use crate::types::models::stats::Stats; pub fn router_config(cfg: &mut web::ServiceConfig) { cfg diff --git a/src/http/endpoints/tags.rs b/src/http/endpoints/tags.rs index 03cded4f..ae527d33 100644 --- a/src/http/endpoints/tags.rs +++ b/src/http/endpoints/tags.rs @@ -2,8 +2,8 @@ use actix_web::{Responder, get, web}; use crate::config::AppData; use crate::database::repository::mod_tags; -use crate::http::endpoints::ApiError; -use crate::types::api::ApiResponse; +use crate::http::error::ApiError; +use crate::http::response::ApiResponse; use crate::types::models::tag::Tag; pub fn router_config(cfg: &mut web::ServiceConfig) { diff --git a/src/http/error.rs b/src/http/error.rs new file mode 100644 index 00000000..3413cb2e --- /dev/null +++ b/src/http/error.rs @@ -0,0 +1,178 @@ +use crate::{ + http::response::ApiResponse, mod_zip::ModZipError, + types::models::mod_gd_version::PlatformParseError, +}; +use actix_web::{HttpRequest, HttpResponse, error::QueryPayloadError, http::StatusCode}; +use image::ImageError; +use validator::{ValidationError, ValidationErrors}; +use zip::result::ZipError; + +#[derive(thiserror::Error, Debug)] +pub enum ApiError { + #[error("Authentication error: {0}")] + Authentication(#[from] crate::auth::AuthenticationError), + #[error("You do not have access to this resource")] + Authorization, + #[error("{0}")] + Database(#[from] crate::database::DatabaseError), + #[error("{0}")] + Storage(#[from] crate::storage::StorageError), + #[error("{0}")] + ModZip(#[from] ModZipError), + #[error("Database error")] + SqlxError(#[from] sqlx::Error), + #[error("Failed to parse response data: {0}")] + Json(#[from] serde_json::Error), + #[error("{0}")] + BadRequest(String), + #[error("{0}")] + #[allow(dead_code)] + TooManyRequests(String), + #[error("Internal error: {0}")] + InternalError(String), + #[error("{0}")] + NotFound(String), + #[error("Error: {0}")] + PlatformParseError(#[from] PlatformParseError), + #[error("Unable to unzip archive")] + 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), + #[error("You are banned from accessing this resource: {}", .0.as_deref().unwrap_or("No reason provided"))] + Banned(Option), +} + +impl ApiError { + pub fn as_response(&self) -> ApiResponse { + ApiResponse { + error: self.to_string(), + payload: "".into(), + } + } +} + +impl actix_web::ResponseError for ApiError { + fn status_code(&self) -> StatusCode { + match self { + ApiError::Authentication(..) => StatusCode::UNAUTHORIZED, + ApiError::Authorization => StatusCode::FORBIDDEN, + ApiError::Json(..) => StatusCode::BAD_REQUEST, + ApiError::TooManyRequests(..) => StatusCode::TOO_MANY_REQUESTS, + ApiError::NotFound(_) => StatusCode::NOT_FOUND, + ApiError::BadRequest(..) => StatusCode::BAD_REQUEST, + ApiError::ModZip(err) => mod_zip_status_code(err), + ApiError::PlatformParseError(_) => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json(self.as_response()) + } +} + +fn mod_zip_status_code(err: &ModZipError) -> StatusCode { + match err { + ModZipError::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ModZipError::ImageError(e) => match e { + ImageError::Limits(_) => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }, + ModZipError::ZipError(e) => match e { + ZipError::InvalidArchive(_) => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }, + _ => StatusCode::BAD_REQUEST, + } +} + +pub fn query_error_handler(err: QueryPayloadError, _req: &HttpRequest) -> actix_web::Error { + ApiError::BadRequest(err.to_string()).into() +} + +// validator errors + +impl From for ApiError { + fn from(value: ValidationError) -> Self { + ApiError::BadRequest(format_validation_error(&value)) + } +} + +impl From for ApiError { + fn from(value: ValidationErrors) -> Self { + ApiError::BadRequest(format_validation_errors(&value)) + } +} + +pub fn format_validation_error(e: &validator::ValidationError) -> String { + if let Some(msg) = &e.message { + return msg.to_string(); + } + + match e.code.as_ref() { + "length" => { + let min = e + .params + .get("min") + .and_then(|v| v.as_u64()) + .map(|v| v.to_string()); + + let max = e + .params + .get("max") + .and_then(|v| v.as_u64()) + .map(|v| v.to_string()); + + match (min, max) { + (Some(min), Some(max)) => { + format!("length must be between {min} and {max} characters") + } + (Some(min), None) => format!("length must be at least {min} characters"), + (None, Some(max)) => format!("length must be at most {max} characters"), + (None, None) => "invalid length".to_string(), + } + } + + e => e.to_owned(), + } +} + +pub fn format_validation_errors(e: &validator::ValidationErrors) -> String { + use validator::ValidationErrorsKind; + + let mut str_errors = Vec::new(); + + for (field, err) in e.errors() { + match err { + ValidationErrorsKind::Struct(s) => { + str_errors.push(format!( + "field '{field}' is invalid ({})", + format_validation_errors(s) + )); + } + + ValidationErrorsKind::Field(errors) => { + let joined = errors + .iter() + .map(format_validation_error) + .collect::>() + .join(", "); + + str_errors.push(format!("field '{field}' is invalid: {}", joined)); + } + + ValidationErrorsKind::List(map) => { + for (index, errors) in map { + str_errors.push(format!( + "field '{field}' at index {index} is invalid ({})", + format_validation_errors(errors) + )); + } + } + } + } + + str_errors.join(", ") +} diff --git a/src/extractors/auth.rs b/src/http/extractors/auth.rs similarity index 98% rename from src/extractors/auth.rs rename to src/http/extractors/auth.rs index c41b8cde..55223d17 100644 --- a/src/extractors/auth.rs +++ b/src/http/extractors/auth.rs @@ -3,7 +3,7 @@ use std::pin::Pin; use crate::auth::AuthenticationError; use crate::config::AppData; use crate::database::repository::developers; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use crate::types::models::developer::Developer; use actix_web::http::header::HeaderMap; use actix_web::{FromRequest, HttpRequest, web}; diff --git a/src/extractors/mod.rs b/src/http/extractors/mod.rs similarity index 100% rename from src/extractors/mod.rs rename to src/http/extractors/mod.rs diff --git a/src/http/mod.rs b/src/http/mod.rs index 27781d35..f1e2ea6a 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,6 +1,9 @@ use actix_cors::Cors; use actix_web::{ - App, HttpMessage, HttpServer, body::MessageBody, dev::{Service, ServiceFactory, ServiceRequest, ServiceResponse}, web::{self, QueryConfig}, + App, HttpMessage, HttpServer, + body::MessageBody, + dev::{Service, ServiceFactory, ServiceRequest, ServiceResponse}, + web::{self, QueryConfig}, }; use tracing_actix_web::RequestId; use utoipa::OpenApi; @@ -12,11 +15,15 @@ use crate::{ auth, deprecations, developers, health, loader, mod_status_badge, mod_version_submissions, mod_versions, mods, stats, tags, }, - openapi::ApiDoc, - types::api, + http::{error::query_error_handler, openapi::ApiDoc}, }; pub mod endpoints; +pub mod error; +pub mod extractors; +pub mod response; + +mod openapi; tokio::task_local! { pub static REQUEST_ID: RequestId; @@ -61,7 +68,7 @@ pub fn create_app( > { let app = App::new() .app_data(data.clone()) - .app_data(QueryConfig::default().error_handler(api::query_error_handler)) + .app_data(QueryConfig::default().error_handler(query_error_handler)) .service(SwaggerUi::new("/swagger/{_:.*}").url("/swagger/openapi.json", openapi.clone())) .wrap(cors_settings()) .wrap_fn(|req, srv| { @@ -70,7 +77,7 @@ pub fn create_app( async move { match request_id { Some(id) => REQUEST_ID.scope(id, fut).await, - None => fut.await + None => fut.await, } } }) diff --git a/src/openapi.rs b/src/http/openapi.rs similarity index 95% rename from src/openapi.rs rename to src/http/openapi.rs index 2bbf7fe6..6420fb30 100644 --- a/src/openapi.rs +++ b/src/http/openapi.rs @@ -1,6 +1,6 @@ use utoipa::OpenApi; -use crate::{http::endpoints, types}; +use crate::{http, http::endpoints, types}; /// Routes served under the `/v1` scope. Their paths are relative to it, like the actix macros. #[derive(OpenApi)] @@ -66,7 +66,7 @@ struct V1Api; nest((path = "/v1", api = V1Api)), components( schemas( - types::api::ApiResponse, + http::response::ApiResponse, types::api::PaginatedData, types::api::PaginatedData, types::api::PaginatedData, @@ -97,9 +97,9 @@ struct V1Api; 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, + types::models::mod_entity::IndexSortType, + types::models::mod_entity::SimpleDevMod, + types::models::mod_entity::SimpleDevModVersion, ) ), tags( diff --git a/src/http/response.rs b/src/http/response.rs new file mode 100644 index 00000000..4578d81e --- /dev/null +++ b/src/http/response.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct ApiResponse { + pub error: String, + pub payload: T, +} diff --git a/src/jobs/cleanup_downloads.rs b/src/jobs/cleanup_downloads.rs index 5dc78afc..864a11db 100644 --- a/src/jobs/cleanup_downloads.rs +++ b/src/jobs/cleanup_downloads.rs @@ -1,5 +1,5 @@ use crate::database::repository::mod_downloads; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use sqlx::PgConnection; pub async fn cleanup_downloads(conn: &mut PgConnection) -> Result<(), ApiError> { diff --git a/src/jobs/logout_user.rs b/src/jobs/logout_user.rs index 575a55b0..dcc9c9cc 100644 --- a/src/jobs/logout_user.rs +++ b/src/jobs/logout_user.rs @@ -1,5 +1,5 @@ use crate::database::repository::{auth_tokens, developers, refresh_tokens}; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use sqlx::PgConnection; pub async fn logout_user(username: &str, conn: &mut PgConnection) -> Result<(), ApiError> { diff --git a/src/jobs/token_cleanup.rs b/src/jobs/token_cleanup.rs index 71ece5a9..337100da 100644 --- a/src/jobs/token_cleanup.rs +++ b/src/jobs/token_cleanup.rs @@ -1,5 +1,5 @@ use crate::database::repository::{auth_tokens, refresh_tokens}; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use sqlx::PgConnection; pub async fn token_cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { diff --git a/src/main.rs b/src/main.rs index 2d5ab932..7344a586 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,12 +7,10 @@ mod config; mod database; mod dns; mod events; -mod extractors; mod http; mod jobs; mod logging; mod mod_zip; -mod openapi; mod s3_worker; mod storage; mod types; diff --git a/src/mod_zip.rs b/src/mod_zip.rs index 16dedad6..cc2185ce 100644 --- a/src/mod_zip.rs +++ b/src/mod_zip.rs @@ -1,8 +1,7 @@ use std::io::Seek; use std::io::{BufReader, Cursor, Read}; -use actix_web::http::StatusCode; -use actix_web::web::Bytes; +use bytes::Bytes; use image::codecs::png::PngDecoder; use image::codecs::png::PngEncoder; use image::{DynamicImage, GenericImageView}; @@ -40,23 +39,6 @@ pub enum ModZipError { InvalidBinaries(String), } -impl ModZipError { - pub fn status_code(&self) -> StatusCode { - match self { - ModZipError::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR, - ModZipError::ImageError(e) => match e { - ImageError::Limits(_) => StatusCode::BAD_REQUEST, - _ => StatusCode::INTERNAL_SERVER_ERROR, - }, - ModZipError::ZipError(e) => match e { - ZipError::InvalidArchive(_) => StatusCode::BAD_REQUEST, - _ => StatusCode::INTERNAL_SERVER_ERROR, - }, - _ => StatusCode::BAD_REQUEST, - } - } -} - pub fn extract_mod_logo(file: &mut ZipFile) -> Result, ModZipError> { const FIVE_MEGABYTES: u64 = 5 * 1000 * 1000; if file.size() > FIVE_MEGABYTES { diff --git a/src/types/api.rs b/src/types/api.rs index 04e87e47..ec02a538 100644 --- a/src/types/api.rs +++ b/src/types/api.rs @@ -1,25 +1,12 @@ -use actix_web::{HttpRequest, error::QueryPayloadError}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::http::endpoints::ApiError; - #[derive(Serialize, Deserialize, Clone, ToSchema)] pub struct PaginatedData { pub data: Vec, pub count: i64, } -#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] -pub struct ApiResponse { - pub error: String, - pub payload: T, -} - -pub fn query_error_handler(err: QueryPayloadError, _req: &HttpRequest) -> actix_web::Error { - ApiError::BadRequest(err.to_string()).into() -} - pub fn create_download_link(app_url: &str, mod_id: &str, version: &str) -> String { format!( "{}/v1/mods/{}/versions/{}/download", diff --git a/src/types/mod_json.rs b/src/types/mod_json.rs index da9bb457..05c7dc63 100644 --- a/src/types/mod_json.rs +++ b/src/types/mod_json.rs @@ -8,7 +8,7 @@ use serde::Deserialize; use validator::{Validate, ValidationError}; use zip::read::ZipFile; -use crate::http::endpoints::format_validation_errors; +use crate::http::error::format_validation_errors; use crate::mod_zip::{self, ModZipError}; use super::models::{ diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index 61ff3ec3..449cbb50 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -9,13 +9,9 @@ use super::{ }; use crate::{ database::{DatabaseError, repository::developers}, - http::endpoints::ApiError, + http::error::ApiError, }; use crate::{ - http::endpoints::{ - developers::{SimpleDevMod, SimpleDevModVersion}, - mods::{IndexQueryParams, IndexSortType}, - }, types::{ api::PaginatedData, models::{mod_version::ModVersion, mod_version_status::ModVersionStatusEnum}, @@ -23,13 +19,64 @@ use crate::{ }, }; use semver::Version; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sqlx::{ PgConnection, types::chrono::{DateTime, Utc}, }; use std::collections::HashMap; -use utoipa::ToSchema; +use utoipa::{IntoParams, ToSchema}; + +#[derive(Deserialize, Default, Hash, Eq, PartialEq, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum IndexSortType { + #[default] + Downloads, + RecentlyUpdated, + RecentlyPublished, + Oldest, + Name, + NameReverse, + Random, +} + +#[derive(Deserialize, Hash, Eq, PartialEq, IntoParams)] +pub struct IndexQueryParams { + pub page: Option, + pub per_page: Option, + pub query: Option, + #[serde(default)] + pub gd: Option, + #[serde(default)] + pub platforms: Option, + #[serde(default)] + pub sort: IndexSortType, + pub geode: Option, + pub developer: Option, + pub tags: Option, + pub featured: Option, + pub jitless: Option, + pub status: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)] +pub struct SimpleDevMod { + pub id: String, + pub featured: bool, + pub download_count: i32, + pub versions: Vec, + pub developers: Vec, +} + +#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)] +pub struct SimpleDevModVersion { + pub name: String, + pub version: String, + pub download_count: i32, + pub validated: bool, + pub info: Option, + pub status: ModVersionStatusEnum, +} #[derive(Serialize, Debug, Clone, sqlx::FromRow, ToSchema)] pub struct Mod { diff --git a/src/types/models/stats.rs b/src/types/models/stats.rs index 233d39ab..bd1a5e82 100644 --- a/src/types/models/stats.rs +++ b/src/types/models/stats.rs @@ -1,4 +1,4 @@ -use crate::{database::repository::developers, http::endpoints::ApiError}; +use crate::{database::repository::developers, http::error::ApiError}; use chrono::Utc; use reqwest::{Client, header::HeaderValue}; use serde::{Deserialize, Serialize}; diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index 604a0b54..27f8ea8b 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::database::{DatabaseError, repository::mod_tags}; -use crate::http::endpoints::ApiError; +use crate::http::error::ApiError; use sqlx::PgConnection; use utoipa::ToSchema; From 0385af0aa89992d819ace2dbc23388dd6b4feeb8 Mon Sep 17 00:00:00 2001 From: Fleeym Date: Fri, 23 Oct 2026 01:02:52 +0300 Subject: [PATCH 8/8] fix: keep actix imports to http module only --- src/cli/serve.rs | 5 +++-- src/http/mod.rs | 9 ++++++--- src/jobs/cleanup_downloads.rs | 3 +-- src/jobs/logout_user.rs | 5 ++--- src/jobs/token_cleanup.rs | 3 +-- src/s3_worker.rs | 10 +++++----- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/cli/serve.rs b/src/cli/serve.rs index ee11dcdc..e585aa8a 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,4 +1,5 @@ -use actix_web::web; +use std::sync::Arc; + use tracing::Instrument; use crate::{config::AppData, database, http, s3_worker}; @@ -22,7 +23,7 @@ pub async fn run(args: ServeArgs, data: AppData) -> anyhow::Result<()> { database::migrate(data.db()).await?; } - let data = web::Data::new(data); + let data = Arc::new(data); tokio::spawn( s3_worker::run_s3_worker(data.clone()).instrument(tracing::info_span!("s3_worker")), ); diff --git a/src/http/mod.rs b/src/http/mod.rs index f1e2ea6a..0383d4df 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -5,9 +5,11 @@ use actix_web::{ dev::{Service, ServiceFactory, ServiceRequest, ServiceResponse}, web::{self, QueryConfig}, }; +use std::sync::Arc; use tracing_actix_web::RequestId; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use uuid::Uuid; use crate::{ config::AppData, @@ -26,7 +28,7 @@ pub mod response; mod openapi; tokio::task_local! { - pub static REQUEST_ID: RequestId; + pub static REQUEST_ID: Uuid; } fn router_config(cfg: &mut web::ServiceConfig) { @@ -72,7 +74,7 @@ pub fn create_app( .service(SwaggerUi::new("/swagger/{_:.*}").url("/swagger/openapi.json", openapi.clone())) .wrap(cors_settings()) .wrap_fn(|req, srv| { - let request_id = req.extensions().get::().copied(); + let request_id = req.extensions().get::().map(|id| **id); let fut = srv.call(req); async move { match request_id { @@ -91,7 +93,8 @@ pub fn create_app( app.configure(router_config) } -pub async fn serve_http(data: web::Data) -> anyhow::Result<()> { +pub async fn serve_http(data: Arc) -> anyhow::Result<()> { + let data = web::Data::from(data); let port = data.port(); let debug = data.debug(); diff --git a/src/jobs/cleanup_downloads.rs b/src/jobs/cleanup_downloads.rs index 864a11db..e7fdbd79 100644 --- a/src/jobs/cleanup_downloads.rs +++ b/src/jobs/cleanup_downloads.rs @@ -1,8 +1,7 @@ use crate::database::repository::mod_downloads; -use crate::http::error::ApiError; use sqlx::PgConnection; -pub async fn cleanup_downloads(conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn cleanup_downloads(conn: &mut PgConnection) -> anyhow::Result<()> { mod_downloads::cleanup(conn).await?; Ok(()) diff --git a/src/jobs/logout_user.rs b/src/jobs/logout_user.rs index dcc9c9cc..47c528a4 100644 --- a/src/jobs/logout_user.rs +++ b/src/jobs/logout_user.rs @@ -1,11 +1,10 @@ use crate::database::repository::{auth_tokens, developers, refresh_tokens}; -use crate::http::error::ApiError; use sqlx::PgConnection; -pub async fn logout_user(username: &str, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn logout_user(username: &str, conn: &mut PgConnection) -> anyhow::Result<()> { let dev = developers::get_one_by_username(username, conn) .await? - .ok_or(ApiError::NotFound("Developer not found".into()))?; + .ok_or_else(|| anyhow::anyhow!("Developer {username} not found"))?; auth_tokens::remove_developer_tokens(dev.id, conn).await?; refresh_tokens::remove_developer_tokens(dev.id, conn).await?; diff --git a/src/jobs/token_cleanup.rs b/src/jobs/token_cleanup.rs index 337100da..a8f7ff36 100644 --- a/src/jobs/token_cleanup.rs +++ b/src/jobs/token_cleanup.rs @@ -1,8 +1,7 @@ use crate::database::repository::{auth_tokens, refresh_tokens}; -use crate::http::error::ApiError; use sqlx::PgConnection; -pub async fn token_cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn token_cleanup(conn: &mut PgConnection) -> anyhow::Result<()> { auth_tokens::cleanup(conn).await?; refresh_tokens::cleanup(conn).await?; diff --git a/src/s3_worker.rs b/src/s3_worker.rs index 807391e5..45faca4f 100644 --- a/src/s3_worker.rs +++ b/src/s3_worker.rs @@ -1,10 +1,10 @@ +use std::sync::Arc; use std::time::Duration; -use actix_web::web; use bytes::Bytes; use reqwest::StatusCode; use sqlx::PgConnection; -use tracing_actix_web::RequestId; +use uuid::Uuid; use crate::{ config::AppData, @@ -22,7 +22,7 @@ use crate::{ pub(crate) struct QueuedS3Task { pub task: S3WorkerTask, - pub request_id: Option, + pub request_id: Option, } pub enum S3WorkerTask { @@ -250,7 +250,7 @@ async fn process_task( data: &AppData, task: S3WorkerTask, is_migration: bool, - request_id: Option, + request_id: Option, ) -> anyhow::Result<()> { let span = tracing::Span::current(); @@ -528,7 +528,7 @@ async fn migrate_loader_versions_to_s3(data: &AppData) -> anyhow::Result<()> { Ok(()) } -pub async fn run_s3_worker(data: web::Data) { +pub async fn run_s3_worker(data: Arc) { if data.cdn_storage().is_none() { return; }