Skip to content
Draft
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.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE geode_versions DROP COLUMN downloads;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE geode_versions ADD COLUMN downloads JSONB;
10 changes: 5 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct AppData {
static_storage: PublicDisk,
public_storage: PublicDisk,
private_storage: PrivateDisk,
mod_storage: Option<PublicDisk>,
cdn_storage: Option<PublicDisk>,
disable_downloads: bool,
max_download_mb: u32,
port: u16,
Expand Down Expand Up @@ -77,7 +77,7 @@ pub async fn build_config() -> anyhow::Result<AppData> {
.time_to_live(Duration::from_mins(10))
.build();

let mod_storage = if let Some(s3_config) = S3Configuration::from_env()? {
let cdn_storage = if let Some(s3_config) = S3Configuration::from_env()? {
let backend = Arc::new(S3Backend::new(&s3_config)?);
Some(PublicDisk::new(backend, s3_config.public_url))
} else {
Expand All @@ -103,7 +103,7 @@ pub async fn build_config() -> anyhow::Result<AppData> {
format!("{app_url}/storage"),
),
private_storage: PrivateDisk::new(Arc::new(LocalBackend::new("storage/private"))),
mod_storage,
cdn_storage,
disable_downloads,
max_download_mb,
port,
Expand Down Expand Up @@ -181,8 +181,8 @@ impl AppData {
&self.private_storage
}

pub fn mod_storage(&self) -> Option<&PublicDisk> {
self.mod_storage.as_ref()
pub fn cdn_storage(&self) -> Option<&PublicDisk> {
self.cdn_storage.as_ref()
}

pub fn mods_cache(&self) -> &Cache<IndexQueryParams, ApiResponse<PaginatedData<Mod>>> {
Expand Down
22 changes: 22 additions & 0 deletions src/database/repository/geode_versions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use sqlx::PgConnection;

use crate::{database::DatabaseError, types::models::loader_version::LoaderDownloads};

#[tracing::instrument(skip_all, fields(tag = %tag, downloads = ?downloads))]
pub async fn update_downloads(
tag: &str,
downloads: &LoaderDownloads,
conn: &mut PgConnection,
) -> Result<(), DatabaseError> {
sqlx::query!(
"UPDATE geode_versions SET downloads = $1 WHERE tag = $2",
// should never fail
&serde_json::to_value(downloads).expect("serialization failed"),
tag
)
.execute(&mut *conn)
.await
.inspect_err(|e| tracing::error!("{:?}", e))?;

Ok(())
}
1 change: 1 addition & 0 deletions src/database/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod auth_tokens;
pub mod dependencies;
pub mod deprecations;
pub mod developers;
pub mod geode_versions;
pub mod github_login_attempts;
pub mod github_web_logins;
pub mod incompatibilities;
Expand Down
9 changes: 7 additions & 2 deletions src/endpoints/loader.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use actix_web::{HttpResponse, Responder, get, post, web};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use utoipa::{IntoParams, ToSchema};

use sqlx::Acquire;

use crate::endpoints::ApiError;
use crate::s3_worker::S3WorkerTask;
use crate::{
config::AppData,
extractors::auth::Auth,
Expand Down Expand Up @@ -122,10 +123,12 @@ pub async fn create_version(
return Err(ApiError::Authorization);
}

let tag = payload.tag.trim_start_matches('v').to_string();

let mut tx = pool.begin().await?;
LoaderVersion::create_version(
LoaderVersionCreate {
tag: payload.tag.trim_start_matches('v').to_string(),
tag: tag.clone(),
prerelease: payload.prerelease,
commit_hash: payload.commit_hash.clone(),
win: payload.gd.win,
Expand All @@ -139,6 +142,8 @@ pub async fn create_version(

tx.commit().await?;

data.send_s3_task(S3WorkerTask::UploadLoader { tag });

Ok(HttpResponse::NoContent())
}

Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/mod_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ pub async fn download_version(
let url = mod_version
.managed_download_link
.as_deref()
.take_if(|_| data.mod_storage().is_some())
.take_if(|_| data.cdn_storage().is_some())
.unwrap_or(&mod_version.download_link);

if data.disable_downloads() || mod_version.status != ModVersionStatusEnum::Accepted {
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ async fn main() -> anyhow::Result<()> {
let app_data = config::build_config().await?;
app_data.static_storage().init().await?;
app_data.private_storage().init().await?;
if let Some(mod_storage) = app_data.mod_storage() {
mod_storage.init().await?;
if let Some(cdn_storage) = app_data.cdn_storage() {
cdn_storage.init().await?;
}

if cli::maybe_cli(&app_data).await? {
Expand Down
127 changes: 122 additions & 5 deletions src/s3_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ use actix_web::web;
use bytes::Bytes;

use crate::{
config::AppData, database::repository::mod_versions::update_managed_download_link, mod_zip,
types::models::mod_gd_version::GDVersionEnum,
config::AppData,
database::repository::{
geode_versions::update_downloads, mod_versions::update_managed_download_link,
},
mod_zip,
types::models::{loader_version::LoaderDownloads, mod_gd_version::GDVersionEnum},
};

pub enum S3WorkerTask {
Expand All @@ -15,14 +19,36 @@ pub enum S3WorkerTask {
version: String,
version_id: i32,
},

UploadLoader {
tag: String,
},
}

fn path_for_mod(mod_id: &str, version: &str) -> String {
format!("mods/{mod_id}/{version}/{mod_id}.geode")
}

fn path_for_loader(tag: &str, platform: &str) -> String {
format!("geode/{tag}/geode-v{tag}-{platform}.zip")
}

fn path_for_resources(tag: &str) -> String {
format!("geode/{tag}/resources.zip")
}

fn github_url_for_loader(tag: &str, platform: &str) -> String {
format!(
"https://github.com/geode-sdk/geode/releases/download/v{tag}/geode-v{tag}-{platform}.zip"
)
}

fn github_url_for_resources(tag: &str) -> String {
format!("https://github.com/geode-sdk/geode/releases/download/v{tag}/resources.zip")
}

async fn process_task(data: &AppData, task: S3WorkerTask) -> anyhow::Result<()> {
let storage = data.mod_storage().expect("mod storage must be set by now");
let storage = data.cdn_storage().expect("mod storage must be set by now");

match task {
S3WorkerTask::UploadMod {
Expand All @@ -47,14 +73,73 @@ async fn process_task(data: &AppData, task: S3WorkerTask) -> anyhow::Result<()>
public_url
);
}

S3WorkerTask::UploadLoader { tag } => {
tracing::info!("Preparing to upload Geode v{tag} to S3");

let mut zips = Vec::new();
let mut downloads = LoaderDownloads::default();

zips.push((
&mut downloads.win,
path_for_loader(&tag, "win"),
github_url_for_loader(&tag, "win"),
));
zips.push((
&mut downloads.mac,
path_for_loader(&tag, "mac"),
github_url_for_loader(&tag, "mac"),
));
zips.push((
&mut downloads.android32,
path_for_loader(&tag, "android32"),
github_url_for_loader(&tag, "android32"),
));
zips.push((
&mut downloads.android64,
path_for_loader(&tag, "android64"),
github_url_for_loader(&tag, "android64"),
));
zips.push((
&mut downloads.ios,
path_for_loader(&tag, "ios"),
github_url_for_loader(&tag, "ios"),
));
zips.push((
&mut downloads.resources,
path_for_resources(&tag),
github_url_for_resources(&tag),
));

for (dl, path, url) in zips {
let resp = data.http_client().get(&url).send().await?;
if let Err(e) = resp.error_for_status_ref() {
tracing::error!("Failed to download loader release from {url}: {e:?}",);
continue;
}

let resp = resp.bytes().await?;
let public_url = storage.asset_url(&path);
storage.store(&path, &resp).await?;

dl.url = public_url;
dl.hash = sha256::digest(&resp[..]);
}

let mut tx = data.db().begin().await?;
update_downloads(&tag, &downloads, &mut tx).await?;
tx.commit().await?;

tracing::info!("Uploaded new loader release to S3: {downloads:?}");
}
}

Ok(())
}

async fn cleanup_old_s3_files(data: &AppData) -> anyhow::Result<()> {
let supported_gd = GDVersionEnum::supported_for_storage();
let storage = data.mod_storage().expect("mod storage must be set by now");
let storage = data.cdn_storage().expect("mod storage must be set by now");

let mut db = data.db().acquire().await?;

Expand Down Expand Up @@ -173,8 +258,33 @@ async fn migrate_existing_mods_to_s3(data: &AppData) -> anyhow::Result<()> {
Ok(())
}

async fn migrate_loader_versions_to_s3(data: &AppData) -> anyhow::Result<()> {
let mut db = data.db().acquire().await?;

let versions = sqlx::query!("SELECT tag FROM geode_versions WHERE downloads IS NULL")
.fetch_all(&mut *db)
.await?;

tracing::info!("Migrating {} Geode releases to S3", versions.len());

for record in versions {
if let Err(e) = process_task(
data,
S3WorkerTask::UploadLoader {
tag: record.tag.clone(),
},
)
.await
{
tracing::error!("error migrating Geode release {} to S3: {e:?}", record.tag);
}
}

Ok(())
}

pub async fn run_s3_worker(data: web::Data<AppData>) {
if data.mod_storage().is_none() {
if data.cdn_storage().is_none() {
return;
}

Expand All @@ -188,6 +298,13 @@ pub async fn run_s3_worker(data: web::Data<AppData>) {
}
});

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 mut interval = tokio::time::interval(Duration::from_mins(30));

loop {
Expand Down
Loading
Loading