diff --git a/.sqlx/query-93fbb956cac5531785cda8c66b1ad1d97dc7b6fe0651696c859a389547f52227.json b/.sqlx/query-93fbb956cac5531785cda8c66b1ad1d97dc7b6fe0651696c859a389547f52227.json new file mode 100644 index 00000000..e83830ae --- /dev/null +++ b/.sqlx/query-93fbb956cac5531785cda8c66b1ad1d97dc7b6fe0651696c859a389547f52227.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE mods SET image = NULL, image_url = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "93fbb956cac5531785cda8c66b1ad1d97dc7b6fe0651696c859a389547f52227" +} diff --git a/.sqlx/query-2b55fed2c473a9b2b249d9afcaff2d290c453d70fb12bf4eb4f099f099f29aa4.json b/.sqlx/query-b1fd42af677f3e4749008e9183c0e660e334e7ef3f5542557103ee9b1a6e9df4.json similarity index 56% rename from .sqlx/query-2b55fed2c473a9b2b249d9afcaff2d290c453d70fb12bf4eb4f099f099f29aa4.json rename to .sqlx/query-b1fd42af677f3e4749008e9183c0e660e334e7ef3f5542557103ee9b1a6e9df4.json index 94d69b88..2e1114d9 100644 --- a/.sqlx/query-2b55fed2c473a9b2b249d9afcaff2d290c453d70fb12bf4eb4f099f099f29aa4.json +++ b/.sqlx/query-b1fd42af677f3e4749008e9183c0e660e334e7ef3f5542557103ee9b1a6e9df4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n m.image\n FROM mods m\n INNER JOIN mod_versions mv ON mv.mod_id = m.id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE m.id = $1", + "query": "SELECT image FROM mods WHERE id = $1", "describe": { "columns": [ { @@ -24,5 +24,5 @@ true ] }, - "hash": "2b55fed2c473a9b2b249d9afcaff2d290c453d70fb12bf4eb4f099f099f29aa4" + "hash": "b1fd42af677f3e4749008e9183c0e660e334e7ef3f5542557103ee9b1a6e9df4" } diff --git a/.sqlx/query-bb526a06ef5ee18d4af6085b913712c5056b8d064d66f1fada69e2561bd470f4.json b/.sqlx/query-bb526a06ef5ee18d4af6085b913712c5056b8d064d66f1fada69e2561bd470f4.json new file mode 100644 index 00000000..5120d13a --- /dev/null +++ b/.sqlx/query-bb526a06ef5ee18d4af6085b913712c5056b8d064d66f1fada69e2561bd470f4.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n m.image,\n m.image_url\n FROM mods m\n INNER JOIN mod_versions mv ON mv.mod_id = m.id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE m.id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "image", + "type_info": "Bytea", + "origin": { + "Table": { + "table": "mods", + "name": "image" + } + } + }, + { + "ordinal": 1, + "name": "image_url", + "type_info": "Text", + "origin": { + "Table": { + "table": "mods", + "name": "image_url" + } + } + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true, + true + ] + }, + "hash": "bb526a06ef5ee18d4af6085b913712c5056b8d064d66f1fada69e2561bd470f4" +} diff --git a/.sqlx/query-ca89c1c99a2504e0961283423340bc7950009c200db4509f2a356de16a8ec0dc.json b/.sqlx/query-ca89c1c99a2504e0961283423340bc7950009c200db4509f2a356de16a8ec0dc.json new file mode 100644 index 00000000..468d345d --- /dev/null +++ b/.sqlx/query-ca89c1c99a2504e0961283423340bc7950009c200db4509f2a356de16a8ec0dc.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM mods WHERE image IS NOT NULL AND length(image) > 0 AND image_url IS NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text", + "origin": { + "Table": { + "table": "mods", + "name": "id" + } + } + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "ca89c1c99a2504e0961283423340bc7950009c200db4509f2a356de16a8ec0dc" +} diff --git a/.sqlx/query-f2973dac842ac43b7a3edd25905475ed18aaa9a5a2694405a8ce3fe01787a865.json b/.sqlx/query-fd7d6b0c760cd011d6bb9d32ec33d34ca7ca8495e44382ce0479b9cd50cfe532.json similarity index 69% rename from .sqlx/query-f2973dac842ac43b7a3edd25905475ed18aaa9a5a2694405a8ce3fe01787a865.json rename to .sqlx/query-fd7d6b0c760cd011d6bb9d32ec33d34ca7ca8495e44382ce0479b9cd50cfe532.json index 76321748..d1f652e3 100644 --- a/.sqlx/query-f2973dac842ac43b7a3edd25905475ed18aaa9a5a2694405a8ce3fe01787a865.json +++ b/.sqlx/query-fd7d6b0c760cd011d6bb9d32ec33d34ca7ca8495e44382ce0479b9cd50cfe532.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE mods\n SET repository = $1,\n about = $2,\n changelog = $3,\n image = $4,\n updated_at = NOW()\n WHERE id = $5", + "query": "UPDATE mods\n SET repository = $1,\n about = $2,\n changelog = $3,\n image = $4,\n image_url = NULL,\n updated_at = NOW()\n WHERE id = $5", "describe": { "columns": [], "parameters": { @@ -14,5 +14,5 @@ }, "nullable": [] }, - "hash": "f2973dac842ac43b7a3edd25905475ed18aaa9a5a2694405a8ce3fe01787a865" + "hash": "fd7d6b0c760cd011d6bb9d32ec33d34ca7ca8495e44382ce0479b9cd50cfe532" } diff --git a/migrations/20260803203053_add_logo_url.down.sql b/migrations/20260803203053_add_logo_url.down.sql new file mode 100644 index 00000000..046ef790 --- /dev/null +++ b/migrations/20260803203053_add_logo_url.down.sql @@ -0,0 +1 @@ +ALTER TABLE mods DROP COLUMN image_url; diff --git a/migrations/20260803203053_add_logo_url.up.sql b/migrations/20260803203053_add_logo_url.up.sql new file mode 100644 index 00000000..3c12bc16 --- /dev/null +++ b/migrations/20260803203053_add_logo_url.up.sql @@ -0,0 +1 @@ +ALTER TABLE mods ADD COLUMN image_url TEXT; diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index 5191855c..bdf01a55 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -6,6 +6,12 @@ use chrono::{DateTime, Utc}; use sqlx::PgConnection; use std::collections::HashSet; +#[derive(Debug, Clone)] +pub enum ModLogo { + Data(Vec), + Url(String), +} + #[derive(sqlx::FromRow)] struct ModRecordGetOne { id: String, @@ -211,15 +217,17 @@ pub async fn exists_multiple( } #[tracing::instrument(skip_all, fields(mod_id = %id))] -pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result>, DatabaseError> { +pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result, DatabaseError> { struct QueryResult { image: Option>, + image_url: Option, } - let vec = sqlx::query_as!( + let logo = sqlx::query_as!( QueryResult, "SELECT - m.image + m.image, + m.image_url FROM mods m INNER JOIN mod_versions mv ON mv.mod_id = m.id INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id @@ -229,14 +237,19 @@ pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result Result<(), DatabaseError> { + sqlx::query!( + "UPDATE mods SET image = NULL, image_url = $1 WHERE id = $2", + url, + id + ) + .execute(conn) + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; + + Ok(()) +} + /// Used when first version goes from pending to accepted. /// Makes it so versions that stay a lot in pending appear at the top of the newly created lists #[tracing::instrument(skip_all, fields(mod_id = %id))] diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index b6c622b0..cb750541 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -10,6 +10,7 @@ use crate::database::repository::mod_links; use crate::database::repository::mod_tags; use crate::database::repository::mod_versions; use crate::database::repository::mods; +use crate::database::repository::mods::ModLogo; use crate::database::repository::{dependencies, deprecations, mod_version_submissions}; use crate::endpoints::ApiError; use crate::events::mod_created::NewUnverifiedModVersionCreated; @@ -418,10 +419,13 @@ pub async fn get_logo( ) -> Result { use crate::database::repository::*; let mut pool = data.db().acquire().await?; - let image: Option> = mods::get_logo(&path.into_inner(), &mut pool).await?; + let image = mods::get_logo(&path.into_inner(), &mut pool).await?; Ok(match image { - Some(i) => HttpResponse::Ok().content_type("image/png").body(i), + Some(ModLogo::Data(i)) => HttpResponse::Ok().content_type("image/png").body(i), + Some(ModLogo::Url(url)) => HttpResponse::Found() + .append_header(("Location", url)) + .finish(), None => HttpResponse::NotFound().body(""), }) } diff --git a/src/s3_worker.rs b/src/s3_worker.rs index 7a349631..fadf514c 100644 --- a/src/s3_worker.rs +++ b/src/s3_worker.rs @@ -2,9 +2,12 @@ use std::time::Duration; use actix_web::web; use bytes::Bytes; +use sqlx::Connection; use crate::{ - config::AppData, database::repository::mod_versions::update_managed_download_link, mod_zip, + config::AppData, + database::repository::{mod_versions::update_managed_download_link, mods::update_mod_logo_url}, + mod_zip, types::models::mod_gd_version::GDVersionEnum, }; @@ -21,8 +24,41 @@ fn path_for_mod(mod_id: &str, version: &str) -> String { format!("mods/{mod_id}/{version}/{mod_id}.geode") } -async fn process_task(data: &AppData, task: S3WorkerTask) -> anyhow::Result<()> { +fn path_for_mod_logo(mod_id: &str) -> String { + format!("mods/{mod_id}/logo.png") +} + +async fn upload_mod_logo(data: &AppData, mod_id: &str) -> anyhow::Result<()> { let storage = data.mod_storage().expect("mod storage must be set by now"); + let mut db = data.db().acquire().await?; + + let logo_path = path_for_mod_logo(mod_id); + let logo_public_url = storage.asset_url(&logo_path); + + let current_logo = sqlx::query!("SELECT image FROM mods WHERE id = $1", mod_id) + .fetch_optional(&mut *db) + .await?; + + if let Some(logo_bytes) = current_logo.and_then(|r| r.image) { + storage.store(&logo_path, &logo_bytes).await?; + + let mut tx = db.begin().await?; + update_mod_logo_url(mod_id, &logo_public_url, &mut tx).await?; + tx.commit().await?; + + tracing::info!("Uploaded logo for {} to S3 at {}", mod_id, logo_public_url); + } + + Ok(()) +} + +async fn process_task( + data: &AppData, + task: S3WorkerTask, + is_migration: bool, +) -> anyhow::Result<()> { + let storage = data.mod_storage().expect("mod storage must be set by now"); + let mut db = data.db().acquire().await?; match task { S3WorkerTask::UploadMod { @@ -36,10 +72,15 @@ async fn process_task(data: &AppData, task: S3WorkerTask) -> anyhow::Result<()> storage.store(&path, &bytes).await?; - let mut tx = data.db().begin().await?; + let mut tx = db.begin().await?; update_managed_download_link(version_id, Some(&public_url), &mut tx).await?; tx.commit().await?; + // upload logo if not migrating mods + if !is_migration { + upload_mod_logo(data, &mod_id).await?; + } + tracing::info!( "Uploaded mod {} {} to S3 at {}", mod_id, @@ -125,6 +166,7 @@ async fn migrate_one( version: version.to_owned(), version_id, }, + true, ) .await } @@ -174,6 +216,21 @@ async fn migrate_existing_mods_to_s3(data: &AppData) -> anyhow::Result<()> { } } + // independently migrate mod logos + let mods = sqlx::query!( + "SELECT id FROM mods WHERE image IS NOT NULL AND length(image) > 0 AND image_url IS NULL" + ) + .fetch_all(&mut *db) + .await?; + + tracing::info!("Migrating {} existing mod logos to S3", mods.len()); + + for record in mods { + if let Err(e) = upload_mod_logo(data, &record.id).await { + tracing::error!("error migrating mod logo for {} to S3: {e:?}", record.id); + } + } + Ok(()) } @@ -197,7 +254,7 @@ pub async fn run_s3_worker(data: web::Data) { loop { let result = tokio::select! { task = rx.recv() => match task { - Some(task) => process_task(&data, task).await, + Some(task) => process_task(&data, task, false).await, None => break, },