You've already forked AstralRinth
forked from didirus/AstralRinth
move to monorepo dir
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
use crate::auth::checks::{is_visible_project, is_visible_version};
|
||||
use crate::database::models::legacy_loader_fields::MinecraftGameVersion;
|
||||
use crate::database::models::loader_fields::Loader;
|
||||
use crate::database::models::project_item::QueryProject;
|
||||
use crate::database::models::version_item::{QueryFile, QueryVersion};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::projects::{ProjectId, VersionId};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::{auth::get_user_from_headers, database};
|
||||
use actix_web::{get, route, web, HttpRequest, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashSet;
|
||||
use yaserde_derive::YaSerialize;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(maven_metadata);
|
||||
cfg.service(version_file_sha512);
|
||||
cfg.service(version_file_sha1);
|
||||
cfg.service(version_file);
|
||||
}
|
||||
|
||||
// TODO: These were modified in v3 and should be tested
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(root = "metadata", rename = "metadata")]
|
||||
pub struct Metadata {
|
||||
#[yaserde(rename = "groupId")]
|
||||
group_id: String,
|
||||
#[yaserde(rename = "artifactId")]
|
||||
artifact_id: String,
|
||||
versioning: Versioning,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(rename = "versioning")]
|
||||
pub struct Versioning {
|
||||
latest: String,
|
||||
release: String,
|
||||
versions: Versions,
|
||||
#[yaserde(rename = "lastUpdated")]
|
||||
last_updated: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(rename = "versions")]
|
||||
pub struct Versions {
|
||||
#[yaserde(rename = "version")]
|
||||
versions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, YaSerialize)]
|
||||
#[yaserde(rename = "project", namespace = "http://maven.apache.org/POM/4.0.0")]
|
||||
pub struct MavenPom {
|
||||
#[yaserde(rename = "xsi:schemaLocation", attribute)]
|
||||
schema_location: String,
|
||||
#[yaserde(rename = "xmlns:xsi", attribute)]
|
||||
xsi: String,
|
||||
#[yaserde(rename = "modelVersion")]
|
||||
model_version: String,
|
||||
#[yaserde(rename = "groupId")]
|
||||
group_id: String,
|
||||
#[yaserde(rename = "artifactId")]
|
||||
artifact_id: String,
|
||||
version: String,
|
||||
name: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[get("maven/modrinth/{id}/maven-metadata.xml")]
|
||||
pub async fn maven_metadata(
|
||||
req: HttpRequest,
|
||||
params: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let project_id = params.into_inner().0;
|
||||
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
|
||||
let user_option = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
let version_names = sqlx::query!(
|
||||
"
|
||||
SELECT id, version_number, version_type
|
||||
FROM versions
|
||||
WHERE mod_id = $1 AND status = ANY($2)
|
||||
ORDER BY ordering ASC NULLS LAST, date_published ASC
|
||||
",
|
||||
project.inner.id as database::models::ids::ProjectId,
|
||||
&*crate::models::projects::VersionStatus::iterator()
|
||||
.filter(|x| x.is_listed())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
|
||||
let mut new_versions = Vec::new();
|
||||
let mut vals = HashSet::new();
|
||||
let mut latest_release = None;
|
||||
|
||||
for row in version_names {
|
||||
let value = if vals.contains(&row.version_number) {
|
||||
format!("{}", VersionId(row.id as u64))
|
||||
} else {
|
||||
row.version_number
|
||||
};
|
||||
|
||||
vals.insert(value.clone());
|
||||
if row.version_type == "release" {
|
||||
latest_release = Some(value.clone())
|
||||
}
|
||||
|
||||
new_versions.push(value);
|
||||
}
|
||||
|
||||
let project_id: ProjectId = project.inner.id.into();
|
||||
|
||||
let respdata = Metadata {
|
||||
group_id: "maven.modrinth".to_string(),
|
||||
artifact_id: project_id.to_string(),
|
||||
versioning: Versioning {
|
||||
latest: new_versions
|
||||
.last()
|
||||
.unwrap_or(&"release".to_string())
|
||||
.to_string(),
|
||||
release: latest_release.unwrap_or_default(),
|
||||
versions: Versions {
|
||||
versions: new_versions,
|
||||
},
|
||||
last_updated: project.inner.updated.format("%Y%m%d%H%M%S").to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/xml")
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?))
|
||||
}
|
||||
|
||||
async fn find_version(
|
||||
project: &QueryProject,
|
||||
vcoords: &String,
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<QueryVersion>, ApiError> {
|
||||
let id_option = crate::models::ids::base62_impl::parse_base62(vcoords)
|
||||
.ok()
|
||||
.map(|x| x as i64);
|
||||
|
||||
let all_versions = database::models::Version::get_many(&project.versions, pool, redis).await?;
|
||||
|
||||
let exact_matches = all_versions
|
||||
.iter()
|
||||
.filter(|x| &x.inner.version_number == vcoords || Some(x.inner.id.0) == id_option)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if exact_matches.len() == 1 {
|
||||
return Ok(Some(exact_matches[0].clone()));
|
||||
}
|
||||
|
||||
// Try to parse version filters from version coords.
|
||||
let Some((vnumber, filter)) = vcoords.rsplit_once('-') else {
|
||||
return Ok(exact_matches.first().map(|x| (*x).clone()));
|
||||
};
|
||||
|
||||
let db_loaders: HashSet<String> = Loader::list(pool, redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.loader)
|
||||
.collect();
|
||||
|
||||
let (loaders, game_versions) = filter
|
||||
.split(',')
|
||||
.map(String::from)
|
||||
.partition::<Vec<_>, _>(|el| db_loaders.contains(el));
|
||||
|
||||
let matched = all_versions
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
let mut bool = x.inner.version_number == vnumber;
|
||||
|
||||
if !loaders.is_empty() {
|
||||
bool &= x.loaders.iter().any(|y| loaders.contains(y));
|
||||
}
|
||||
|
||||
// For maven in particular, we will hardcode it to use GameVersions rather than generic loader fields, as this is minecraft-java exclusive
|
||||
if !game_versions.is_empty() {
|
||||
let version_game_versions = x
|
||||
.version_fields
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find_map(|v| MinecraftGameVersion::try_from_version_field(&v).ok());
|
||||
if let Some(version_game_versions) = version_game_versions {
|
||||
bool &= version_game_versions
|
||||
.iter()
|
||||
.any(|y| game_versions.contains(&y.version));
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(matched
|
||||
.first()
|
||||
.or_else(|| exact_matches.first())
|
||||
.copied()
|
||||
.cloned())
|
||||
}
|
||||
|
||||
fn find_file<'a>(
|
||||
project_id: &str,
|
||||
vcoords: &str,
|
||||
version: &'a QueryVersion,
|
||||
file: &str,
|
||||
) -> Option<&'a QueryFile> {
|
||||
if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) {
|
||||
return Some(selected_file);
|
||||
}
|
||||
|
||||
// Minecraft mods are not going to be both a mod and a modpack, so this minecraft-specific handling is fine
|
||||
// As there can be multiple project types, returns the first allowable match
|
||||
let mut fileexts = vec![];
|
||||
for project_type in version.project_types.iter() {
|
||||
match project_type.as_str() {
|
||||
"mod" => fileexts.push("jar"),
|
||||
"modpack" => fileexts.push("mrpack"),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
for fileext in fileexts {
|
||||
if file == format!("{}-{}.{}", &project_id, &vcoords, fileext) {
|
||||
return version
|
||||
.files
|
||||
.iter()
|
||||
.find(|x| x.primary)
|
||||
.or_else(|| version.files.iter().last());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[route(
|
||||
"maven/modrinth/{id}/{versionnum}/{file}",
|
||||
method = "GET",
|
||||
method = "HEAD"
|
||||
)]
|
||||
pub async fn version_file(
|
||||
req: HttpRequest,
|
||||
params: web::Path<(String, String, String)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (project_id, vnum, file) = params.into_inner();
|
||||
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
|
||||
let user_option = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
|
||||
if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
if file == format!("{}-{}.pom", &project_id, &vnum) {
|
||||
let respdata = MavenPom {
|
||||
schema_location:
|
||||
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
.to_string(),
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance".to_string(),
|
||||
model_version: "4.0.0".to_string(),
|
||||
group_id: "maven.modrinth".to_string(),
|
||||
artifact_id: project_id,
|
||||
version: vnum,
|
||||
name: project.inner.name,
|
||||
description: project.inner.description,
|
||||
};
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("text/xml")
|
||||
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?));
|
||||
} else if let Some(selected_file) = find_file(&project_id, &vnum, &version, &file) {
|
||||
return Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("location", &*selected_file.url))
|
||||
.body(""));
|
||||
}
|
||||
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
|
||||
#[get("maven/modrinth/{id}/{versionnum}/{file}.sha1")]
|
||||
pub async fn version_file_sha1(
|
||||
req: HttpRequest,
|
||||
params: web::Path<(String, String, String)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (project_id, vnum, file) = params.into_inner();
|
||||
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
|
||||
let user_option = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
|
||||
if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
Ok(find_file(&project_id, &vnum, &version, &file)
|
||||
.and_then(|file| file.hashes.get("sha1"))
|
||||
.map(|hash_str| HttpResponse::Ok().body(hash_str.clone()))
|
||||
.unwrap_or_else(|| HttpResponse::NotFound().body("")))
|
||||
}
|
||||
|
||||
#[get("maven/modrinth/{id}/{versionnum}/{file}.sha512")]
|
||||
pub async fn version_file_sha512(
|
||||
req: HttpRequest,
|
||||
params: web::Path<(String, String, String)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (project_id, vnum, file) = params.into_inner();
|
||||
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
|
||||
let user_option = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else {
|
||||
return Err(ApiError::NotFound);
|
||||
};
|
||||
|
||||
if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
Ok(find_file(&project_id, &vnum, &version, &file)
|
||||
.and_then(|file| file.hashes.get("sha512"))
|
||||
.map(|hash_str| HttpResponse::Ok().body(hash_str.clone()))
|
||||
.unwrap_or_else(|| HttpResponse::NotFound().body("")))
|
||||
}
|
||||
Reference in New Issue
Block a user