Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions migrations/20260803203053_add_logo_url.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE mods DROP COLUMN image_url;
1 change: 1 addition & 0 deletions migrations/20260803203053_add_logo_url.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE mods ADD COLUMN image_url TEXT;
53 changes: 43 additions & 10 deletions src/database/repository/mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ use chrono::{DateTime, Utc};
use sqlx::PgConnection;
use std::collections::HashSet;

#[derive(Debug, Clone)]
pub enum ModLogo {
Data(Vec<u8>),
Url(String),
}

#[derive(sqlx::FromRow)]
struct ModRecordGetOne {
id: String,
Expand Down Expand Up @@ -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<Option<Vec<u8>>, DatabaseError> {
pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result<Option<ModLogo>, DatabaseError> {
struct QueryResult {
image: Option<Vec<u8>>,
image_url: Option<String>,
}

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
Expand All @@ -229,14 +237,19 @@ pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result<Option<Vec<u8
.fetch_optional(&mut *conn)
.await
.inspect_err(|e| tracing::error!("{:?}", e))?
.and_then(|optional| optional.image);
.and_then(|r| {
if let Some(url) = r.image_url {
Some(ModLogo::Url(url))
} else if let Some(data) = r.image
&& !data.is_empty()
{
Some(ModLogo::Data(data))
} else {
None
}
});

// Empty vec means no image
if vec.as_ref().is_some_and(|v| v.is_empty()) {
Ok(None)
} else {
Ok(vec)
}
Ok(logo)
}

#[tracing::instrument(skip_all, fields(mod_id = %id))]
Expand Down Expand Up @@ -266,6 +279,7 @@ pub async fn update_with_json_moved(
about = $2,
changelog = $3,
image = $4,
image_url = NULL,
updated_at = NOW()
WHERE id = $5",
json.repository,
Expand All @@ -285,6 +299,25 @@ pub async fn update_with_json_moved(
Ok(the_mod)
}

/// Updates the logo URL in the database and sets the logo data to null.
#[tracing::instrument(skip_all, fields(id = %id, url = %url))]
pub async fn update_mod_logo_url(
id: &str,
url: &str,
conn: &mut PgConnection,
) -> 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))]
Expand Down
8 changes: 6 additions & 2 deletions src/endpoints/mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -418,10 +419,13 @@ pub async fn get_logo(
) -> Result<impl Responder, ApiError> {
use crate::database::repository::*;
let mut pool = data.db().acquire().await?;
let image: Option<Vec<u8>> = 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(""),
})
}
Expand Down
65 changes: 61 additions & 4 deletions src/s3_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -125,6 +166,7 @@ async fn migrate_one(
version: version.to_owned(),
version_id,
},
true,
)
.await
}
Expand Down Expand Up @@ -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(())
}

Expand All @@ -197,7 +254,7 @@ pub async fn run_s3_worker(data: web::Data<AppData>) {
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,
},

Expand Down
Loading