Initial shared instances backend (#3800)

* Create base shared instance migration and initial routes

* Fix build

* Add version uploads

* Add permissions field for shared instance users

* Actually use permissions field

* Add "public" flag to shared instances that allow GETing them without authorization

* Add the ability to get and list shared instance versions

* Add the ability to delete shared instance versions

* Fix build after merge

* Secured file hosting (#3784)

* Remove Backblaze-specific file-hosting backend

* Added S3_USES_PATH_STYLE_BUCKETS

* Remove unused file_id parameter from delete_file_version

* Add support for separate public and private buckets in labrinth::file_hosting

* Rename delete_file_version to delete_file

* Add (untested) get_url_for_private_file

* Remove url field from shared instance routes

* Remove url field from shared instance routes

* Use private bucket for shared instance versions

* Make S3 environment variables fully separate between public and private buckets

* Change file host expiry for shared instances to 180 seconds

* Fix lint

* Merge shared instance migrations into a single migration

* Replace shared instance owners with Ghost instead of deleting the instance
This commit is contained in:
Josiah Glosson
2025-06-19 14:46:12 -05:00
committed by GitHub
parent d4864deac5
commit cc34e69524
61 changed files with 2161 additions and 491 deletions

View File

@@ -1,6 +1,6 @@
use std::str::FromStr;
pub fn parse_var<T: FromStr>(var: &'static str) -> Option<T> {
pub fn parse_var<T: FromStr>(var: &str) -> Option<T> {
dotenvy::var(var).ok().and_then(|i| i.parse().ok())
}
pub fn parse_strings_from_var(var: &'static str) -> Option<Vec<String>> {

View File

@@ -1,3 +1,5 @@
pub const MRPACK_MIME_TYPE: &str = "application/x-modrinth-modpack+zip";
pub fn get_image_content_type(extension: &str) -> Option<&'static str> {
match extension {
"bmp" => Some("image/bmp"),
@@ -24,7 +26,7 @@ pub fn project_file_type(ext: &str) -> Option<&str> {
match ext {
"jar" => Some("application/java-archive"),
"zip" | "litemod" => Some("application/zip"),
"mrpack" => Some("application/x-modrinth-modpack+zip"),
"mrpack" => Some(MRPACK_MIME_TYPE),
_ => None,
}
}

View File

@@ -1,7 +1,7 @@
use crate::database;
use crate::database::models::image_item;
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::images::ImageContext;
use crate::routes::ApiError;
use color_thief::ColorFormat;
@@ -38,11 +38,14 @@ pub struct UploadImageResult {
pub raw_url: String,
pub raw_url_path: String,
pub publicity: FileHostPublicity,
pub color: Option<u32>,
}
pub async fn upload_image_optimized(
upload_folder: &str,
publicity: FileHostPublicity,
bytes: bytes::Bytes,
file_extension: &str,
target_width: Option<u32>,
@@ -80,6 +83,7 @@ pub async fn upload_image_optimized(
target_width.unwrap_or(0),
processed_image_ext
),
publicity,
processed_image,
)
.await?,
@@ -92,6 +96,7 @@ pub async fn upload_image_optimized(
.upload_file(
content_type,
&format!("{upload_folder}/{hash}.{file_extension}"),
publicity,
bytes,
)
.await?;
@@ -107,6 +112,9 @@ pub async fn upload_image_optimized(
raw_url: url,
raw_url_path: upload_data.file_name,
publicity,
color,
})
}
@@ -165,6 +173,7 @@ fn convert_to_webp(img: &DynamicImage) -> Result<Vec<u8>, ImageError> {
pub async fn delete_old_images(
image_url: Option<String>,
raw_image_url: Option<String>,
publicity: FileHostPublicity,
file_host: &dyn FileHost,
) -> Result<(), ApiError> {
let cdn_url = dotenvy::var("CDN_URL")?;
@@ -173,7 +182,7 @@ pub async fn delete_old_images(
let name = image_url.split(&cdn_url_start).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
file_host.delete_file(icon_path, publicity).await?;
}
}
@@ -181,7 +190,7 @@ pub async fn delete_old_images(
let name = raw_image_url.split(&cdn_url_start).nth(1);
if let Some(icon_path) = name {
file_host.delete_file_version("", icon_path).await?;
file_host.delete_file(icon_path, publicity).await?;
}
}

View File

@@ -1,11 +1,14 @@
use crate::routes::ApiError;
use crate::routes::v3::project_creation::CreateError;
use crate::util::validate::validation_errors_to_string;
use actix_multipart::Field;
use actix_web::web::Payload;
use bytes::BytesMut;
use futures::StreamExt;
use serde::de::DeserializeOwned;
use validator::Validate;
pub async fn read_from_payload(
pub async fn read_limited_from_payload(
payload: &mut Payload,
cap: usize,
err_msg: &'static str,
@@ -25,6 +28,28 @@ pub async fn read_from_payload(
Ok(bytes)
}
pub async fn read_typed_from_payload<T>(
payload: &mut Payload,
) -> Result<T, ApiError>
where
T: DeserializeOwned + Validate,
{
let mut bytes = BytesMut::new();
while let Some(item) = payload.next().await {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInput(
"Unable to parse bytes in payload sent!".to_string(),
)
})?);
}
let parsed: T = serde_json::from_slice(&bytes)?;
parsed.validate().map_err(|err| {
ApiError::InvalidInput(validation_errors_to_string(err, None))
})?;
Ok(parsed)
}
pub async fn read_from_field(
field: &mut Field,
cap: usize,