You've already forked AstralRinth
forked from didirus/AstralRinth
Mod Creation (#34)
* Inital creation stuff * File Reader * Upload bodies * Major rework: * Finish Multiple Files * Proper Error Handling * Switch to database models * Run formatter * Make dependencies dependent on Versions over mods * Fixes * Fix clippy * Run lint one last time * Update src/models/mods.rs Co-authored-by: AppleTheGolden <scotsbox@protonmail.com> Co-authored-by: AppleTheGolden <scotsbox@protonmail.com>
This commit is contained in:
2
.env
2
.env
@@ -1,6 +1,8 @@
|
|||||||
INDEX_CURSEFORGE=false
|
INDEX_CURSEFORGE=false
|
||||||
DEBUG=true
|
DEBUG=true
|
||||||
|
|
||||||
|
CDN_URL=cdn.modrinth.com
|
||||||
|
|
||||||
MONGODB_ADDR=mongodb://localhost:27017
|
MONGODB_ADDR=mongodb://localhost:27017
|
||||||
MEILISEARCH_ADDR=http://localhost:7700
|
MEILISEARCH_ADDR=http://localhost:7700
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ edition = "2018"
|
|||||||
actix-web = "2.0"
|
actix-web = "2.0"
|
||||||
actix-rt = "1.1.1"
|
actix-rt = "1.1.1"
|
||||||
actix-files = "0.2.2"
|
actix-files = "0.2.2"
|
||||||
|
actix-multipart = "0.2.0"
|
||||||
|
|
||||||
reqwest = {version="0.10.6", features=["json"]}
|
reqwest = {version="0.10.6", features=["json"]}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod mod_item;
|
mod mod_item;
|
||||||
|
mod team_item;
|
||||||
mod version_item;
|
mod version_item;
|
||||||
|
|
||||||
use crate::database::DatabaseError::NotFound;
|
use crate::database::DatabaseError::NotFound;
|
||||||
@@ -8,7 +9,11 @@ use bson::doc;
|
|||||||
use bson::Document;
|
use bson::Document;
|
||||||
pub use mod_item::Mod;
|
pub use mod_item::Mod;
|
||||||
use mongodb::Database;
|
use mongodb::Database;
|
||||||
|
pub use team_item::Team;
|
||||||
|
pub use team_item::TeamMember;
|
||||||
|
pub use version_item::FileHash;
|
||||||
pub use version_item::Version;
|
pub use version_item::Version;
|
||||||
|
pub use version_item::VersionFile;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Item {
|
pub trait Item {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::database::models::team_item::Team;
|
||||||
use crate::database::models::Item;
|
use crate::database::models::Item;
|
||||||
use crate::database::Result;
|
use crate::database::Result;
|
||||||
use bson::{Bson, Document};
|
use bson::{Bson, Document};
|
||||||
@@ -5,15 +6,23 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Mod {
|
pub struct Mod {
|
||||||
|
/// The ID for the mod, must be serializable to base62
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
//Todo: Move to own table
|
||||||
|
/// The team that owns the mod
|
||||||
|
pub team: Team,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
pub body_url: String,
|
||||||
pub published: String,
|
pub published: String,
|
||||||
pub author: String,
|
|
||||||
pub downloads: i32,
|
pub downloads: i32,
|
||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
pub body_path: String,
|
///A vector of Version IDs specifying the mod version of a dependency
|
||||||
pub icon_path: String,
|
pub version_ids: Vec<i32>,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
|
pub issues_url: Option<String>,
|
||||||
|
pub source_url: Option<String>,
|
||||||
|
pub wiki_url: Option<String>,
|
||||||
}
|
}
|
||||||
impl Item for Mod {
|
impl Item for Mod {
|
||||||
fn get_collection() -> &'static str {
|
fn get_collection() -> &'static str {
|
||||||
|
|||||||
20
src/database/models/team_item.rs
Normal file
20
src/database/models/team_item.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A team of users who control a mod
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Team {
|
||||||
|
/// The id of the team
|
||||||
|
pub id: i32,
|
||||||
|
/// A list of the members of the team
|
||||||
|
pub members: Vec<TeamMember>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A member of a team
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct TeamMember {
|
||||||
|
/// The ID of the user associated with the member
|
||||||
|
pub user_id: i32,
|
||||||
|
/// The name of the user
|
||||||
|
pub name: String,
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
@@ -3,19 +3,39 @@ use crate::database::Result;
|
|||||||
use bson::{Bson, Document};
|
use bson::{Bson, Document};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
//TODO: Files should probably be moved to their own table
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Version {
|
pub struct Version {
|
||||||
|
///The unqiue VersionId of this version
|
||||||
pub version_id: i32,
|
pub version_id: i32,
|
||||||
|
/// The ModId of the mod that this version belongs to
|
||||||
pub mod_id: i32,
|
pub mod_id: i32,
|
||||||
pub title: String,
|
pub name: String,
|
||||||
pub changelog_path: String,
|
pub number: String,
|
||||||
pub files_path: Vec<String>,
|
pub changelog_url: Option<String>,
|
||||||
pub date_published: String,
|
pub date_published: String,
|
||||||
pub author: String,
|
|
||||||
pub downloads: i32,
|
pub downloads: i32,
|
||||||
pub dependencies: Vec<String>,
|
pub files: Vec<VersionFile>,
|
||||||
|
pub dependencies: Vec<i32>,
|
||||||
pub game_versions: Vec<String>,
|
pub game_versions: Vec<String>,
|
||||||
|
pub loaders: Vec<String>,
|
||||||
|
pub version_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct VersionFile {
|
||||||
|
pub game_versions: Vec<String>,
|
||||||
|
pub hashes: Vec<FileHash>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hash of a mod's file
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct FileHash {
|
||||||
|
pub algorithm: String,
|
||||||
|
pub hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl Item for Version {
|
impl Item for Version {
|
||||||
fn get_collection() -> &'static str {
|
fn get_collection() -> &'static str {
|
||||||
"versions"
|
"versions"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub struct AuthorizationData {
|
|||||||
pub recommended_part_size: i32,
|
pub recommended_part_size: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct UploadUrlData {
|
pub struct UploadUrlData {
|
||||||
pub bucket_id: String,
|
pub bucket_id: String,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_authorization() {
|
async fn test_authorization() {
|
||||||
|
println!("{}", dotenv::var("BACKBLAZE_BUCKET_ID").unwrap());
|
||||||
let authorization_data = authorize_account(
|
let authorization_data = authorize_account(
|
||||||
dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
|
dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
|
||||||
dotenv::var("BACKBLAZE_KEY").unwrap(),
|
dotenv::var("BACKBLAZE_KEY").unwrap(),
|
||||||
|
|||||||
22
src/main.rs
22
src/main.rs
@@ -19,9 +19,28 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
check_env_vars();
|
check_env_vars();
|
||||||
|
|
||||||
|
//Database Connecter
|
||||||
let client = database::connect()
|
let client = database::connect()
|
||||||
.await
|
.await
|
||||||
.expect("Database connection failed");
|
.expect("Database connection failed");
|
||||||
|
let client_ref = web::Data::new(client.clone());
|
||||||
|
|
||||||
|
//File Hosting Initializer
|
||||||
|
let authorization_data = file_hosting::authorize_account(
|
||||||
|
dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
|
||||||
|
dotenv::var("BACKBLAZE_KEY").unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let upload_url_data = file_hosting::get_upload_url(
|
||||||
|
authorization_data.clone(),
|
||||||
|
dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let authorization_data_ref = web::Data::new(authorization_data);
|
||||||
|
let upload_url_data_ref = web::Data::new(upload_url_data);
|
||||||
|
|
||||||
// Get executable path
|
// Get executable path
|
||||||
let mut exe_path = env::current_exe()?.parent().unwrap().to_path_buf();
|
let mut exe_path = env::current_exe()?.parent().unwrap().to_path_buf();
|
||||||
@@ -48,6 +67,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
App::new()
|
App::new()
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(Logger::new("%a %{User-Agent}i"))
|
.wrap(Logger::new("%a %{User-Agent}i"))
|
||||||
|
.data(client_ref.clone())
|
||||||
|
.data(authorization_data_ref.clone())
|
||||||
|
.data(upload_url_data_ref.clone())
|
||||||
.service(routes::index_get)
|
.service(routes::index_get)
|
||||||
.service(routes::mod_search)
|
.service(routes::mod_search)
|
||||||
.default_service(web::get().to(routes::not_found))
|
.default_service(web::get().to(routes::not_found))
|
||||||
|
|||||||
@@ -84,12 +84,27 @@ macro_rules! from_base62id {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
from_base62id! {
|
macro_rules! impl_base62_display {
|
||||||
ModId, ModId;
|
($struct:ty) => {
|
||||||
UserId, UserId;
|
impl std::fmt::Display for $struct {
|
||||||
VersionId, VersionId;
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
TeamId, TeamId;
|
f.write_str(&base62_impl::to_base62(self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
impl_base62_display!(Base62Id);
|
||||||
|
|
||||||
|
macro_rules! base62_id_impl {
|
||||||
|
($struct:ty, $cons:expr) => {
|
||||||
|
from_base62id!($struct, $cons;);
|
||||||
|
impl_base62_display!($struct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base62_id_impl!(ModId, ModId);
|
||||||
|
base62_id_impl!(UserId, UserId);
|
||||||
|
base62_id_impl!(VersionId, VersionId);
|
||||||
|
base62_id_impl!(TeamId, TeamId);
|
||||||
|
|
||||||
pub mod base62_impl {
|
pub mod base62_impl {
|
||||||
use serde::de::{self, Deserializer, Visitor};
|
use serde::de::{self, Deserializer, Visitor};
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ pub struct Mod {
|
|||||||
// unnecessary info
|
// unnecessary info
|
||||||
/// The team of people that has ownership of this mod.
|
/// The team of people that has ownership of this mod.
|
||||||
pub team: Team,
|
pub team: Team,
|
||||||
|
|
||||||
/// The title or name of the mod.
|
/// The title or name of the mod.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// A short description of the mod.
|
/// A short description of the mod.
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
/// The link to the long description of the mod.
|
||||||
|
pub body_url: String,
|
||||||
/// The date at which the mod was first published.
|
/// The date at which the mod was first published.
|
||||||
pub published: DateTime<Utc>,
|
pub published: DateTime<Utc>,
|
||||||
|
|
||||||
@@ -38,10 +39,8 @@ pub struct Mod {
|
|||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
/// A list of ids for versions of the mod.
|
/// A list of ids for versions of the mod.
|
||||||
pub versions: Vec<VersionId>,
|
pub versions: Vec<VersionId>,
|
||||||
|
///The URL of the icon of the mod
|
||||||
/// The latest version of the mod.
|
pub icon_url: Option<String>,
|
||||||
pub latest_version: Version,
|
|
||||||
|
|
||||||
/// An optional link to where to submit bugs or issues with the mod.
|
/// An optional link to where to submit bugs or issues with the mod.
|
||||||
pub issues_url: Option<String>,
|
pub issues_url: Option<String>,
|
||||||
/// An optional link to the source code for the mod.
|
/// An optional link to the source code for the mod.
|
||||||
@@ -60,6 +59,8 @@ pub struct Version {
|
|||||||
|
|
||||||
/// The name of this version
|
/// The name of this version
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// The version number. Ideally will follow semantic versioning
|
||||||
|
pub version_number: String,
|
||||||
/// A link to the changelog for this version of the mod.
|
/// A link to the changelog for this version of the mod.
|
||||||
pub changelog_url: Option<String>,
|
pub changelog_url: Option<String>,
|
||||||
/// The date that this version was published.
|
/// The date that this version was published.
|
||||||
@@ -72,9 +73,11 @@ pub struct Version {
|
|||||||
/// A list of files available for download for this version.
|
/// A list of files available for download for this version.
|
||||||
pub files: Vec<VersionFile>,
|
pub files: Vec<VersionFile>,
|
||||||
/// A list of mods that this version depends on.
|
/// A list of mods that this version depends on.
|
||||||
pub dependencies: Vec<ModId>,
|
pub dependencies: Vec<VersionId>,
|
||||||
/// A list of versions of Minecraft that this version of the mod supports.
|
/// A list of versions of Minecraft that this version of the mod supports.
|
||||||
pub game_versions: Vec<GameVersion>,
|
pub game_versions: Vec<GameVersion>,
|
||||||
|
/// The loaders that this version works on
|
||||||
|
pub loaders: Vec<ModLoader>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single mod file, with a url for the file and the file's hash
|
/// A single mod file, with a url for the file and the file's hash
|
||||||
@@ -96,18 +99,34 @@ pub struct FileHash {
|
|||||||
pub hash: String,
|
pub hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub enum VersionType {
|
pub enum VersionType {
|
||||||
Release,
|
Release,
|
||||||
Beta,
|
Beta,
|
||||||
Alpha,
|
Alpha,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToString for VersionType {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
VersionType::Release => "release",
|
||||||
|
VersionType::Beta => "beta",
|
||||||
|
VersionType::Alpha => "alpha",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A specific version of Minecraft
|
/// A specific version of Minecraft
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct GameVersion(pub String);
|
pub struct GameVersion(pub String);
|
||||||
|
|
||||||
|
/// A mod loader
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct ModLoader(pub String);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SearchRequest {
|
pub struct SearchRequest {
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use super::ids::Base62Id;
|
use super::ids::Base62Id;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
//TODO Implement Item for teams
|
||||||
/// The ID of a specific user, encoded as base62 for usage in the API
|
/// The ID of a specific user, encoded as base62 for usage in the API
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(from = "Base62Id")]
|
#[serde(from = "Base62Id")]
|
||||||
@@ -24,10 +25,12 @@ pub struct Team {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A member of a team
|
/// A member of a team
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct TeamMember {
|
pub struct TeamMember {
|
||||||
/// The ID of the user associated with the member
|
/// The ID of the user associated with the member
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
/// The name of the user
|
/// The name of the user
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
///The role of the use in the team
|
||||||
|
pub role: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod index;
|
mod index;
|
||||||
|
mod mod_creation;
|
||||||
mod mods;
|
mod mods;
|
||||||
mod not_found;
|
mod not_found;
|
||||||
|
|
||||||
|
|||||||
416
src/routes/mod_creation.rs
Normal file
416
src/routes/mod_creation.rs
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
use crate::database::models::{FileHash, Mod, Team, Version, VersionFile};
|
||||||
|
use crate::file_hosting::{upload_file, FileHostingError, UploadUrlData};
|
||||||
|
use crate::models::error::ApiError;
|
||||||
|
use crate::models::ids::random_base62;
|
||||||
|
use crate::models::mods::{GameVersion, ModId, VersionId, VersionType};
|
||||||
|
use crate::models::teams::TeamMember;
|
||||||
|
use actix_multipart::{Field, Multipart};
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::{post, HttpResponse};
|
||||||
|
use bson::doc;
|
||||||
|
use bson::Bson;
|
||||||
|
use chrono::Utc;
|
||||||
|
use futures::stream::StreamExt;
|
||||||
|
use mongodb::Client;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CreateError {
|
||||||
|
#[error("Environment Error")]
|
||||||
|
EnvError(#[from] dotenv::Error),
|
||||||
|
#[error("Error while adding project to database")]
|
||||||
|
DatabaseError(#[from] mongodb::error::Error),
|
||||||
|
#[error("Error while parsing multipart payload")]
|
||||||
|
MultipartError(actix_multipart::MultipartError),
|
||||||
|
#[error("Error while parsing JSON")]
|
||||||
|
SerDeError(#[from] serde_json::Error),
|
||||||
|
#[error("Error while uploading file")]
|
||||||
|
FileHostingError(#[from] FileHostingError),
|
||||||
|
#[error("Error while parsing string as UTF-8")]
|
||||||
|
InvalidUtf8Input(#[source] std::string::FromUtf8Error),
|
||||||
|
#[error("{}", .0)]
|
||||||
|
MissingValueError(String),
|
||||||
|
#[error("Error while trying to generate random ID")]
|
||||||
|
RandomIdError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_web::ResponseError for CreateError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
CreateError::FileHostingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
CreateError::SerDeError(..) => StatusCode::BAD_REQUEST,
|
||||||
|
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
|
||||||
|
CreateError::InvalidUtf8Input(..) => StatusCode::BAD_REQUEST,
|
||||||
|
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
|
||||||
|
CreateError::RandomIdError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HttpResponse::build(self.status_code()).json(ApiError {
|
||||||
|
error: match self {
|
||||||
|
CreateError::EnvError(..) => "environment_error",
|
||||||
|
CreateError::DatabaseError(..) => "database_error",
|
||||||
|
CreateError::FileHostingError(..) => "file_hosting_error",
|
||||||
|
CreateError::SerDeError(..) => "invalid_input",
|
||||||
|
CreateError::MultipartError(..) => "invalid_input",
|
||||||
|
CreateError::InvalidUtf8Input(..) => "invalid_input",
|
||||||
|
CreateError::MissingValueError(..) => "invalid_input",
|
||||||
|
CreateError::RandomIdError => "id_generation_error",
|
||||||
|
},
|
||||||
|
description: &self.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
struct InitialVersionData {
|
||||||
|
pub file_indexes: Vec<i32>,
|
||||||
|
pub version_number: String,
|
||||||
|
pub version_title: String,
|
||||||
|
pub version_body: String,
|
||||||
|
pub dependencies: Vec<VersionId>,
|
||||||
|
pub game_versions: Vec<GameVersion>,
|
||||||
|
pub version_type: VersionType,
|
||||||
|
pub loaders: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
struct ModCreateData {
|
||||||
|
/// The title or name of the mod.
|
||||||
|
pub mod_name: String,
|
||||||
|
/// The namespace of the mod
|
||||||
|
pub mod_namespace: String,
|
||||||
|
/// A short description of the mod.
|
||||||
|
pub mod_description: String,
|
||||||
|
/// A long description of the mod, in markdown.
|
||||||
|
pub mod_body: String,
|
||||||
|
/// A list of initial versions to upload with the created mod
|
||||||
|
pub initial_versions: Vec<InitialVersionData>,
|
||||||
|
/// The team of people that has ownership of this mod.
|
||||||
|
pub team_members: Vec<TeamMember>,
|
||||||
|
/// A list of the categories that the mod is in.
|
||||||
|
pub categories: Vec<String>,
|
||||||
|
/// An optional link to where to submit bugs or issues with the mod.
|
||||||
|
pub issues_url: Option<String>,
|
||||||
|
/// An optional link to the source code for the mod.
|
||||||
|
pub source_url: Option<String>,
|
||||||
|
/// An optional link to the mod's wiki page or other relevant information.
|
||||||
|
pub wiki_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("api/v1/mod")]
|
||||||
|
pub async fn mod_create(
|
||||||
|
mut payload: Multipart,
|
||||||
|
client: Data<Client>,
|
||||||
|
upload_url: Data<UploadUrlData>,
|
||||||
|
) -> Result<HttpResponse, CreateError> {
|
||||||
|
//TODO Switch to transactions for safer database and file upload calls (once it is implemented in the APIs)
|
||||||
|
let cdn_url = dotenv::var("CDN_URL")?;
|
||||||
|
|
||||||
|
let db = client.database("modrinth");
|
||||||
|
|
||||||
|
let mods = db.collection("mods");
|
||||||
|
let versions = db.collection("versions");
|
||||||
|
|
||||||
|
let mut mod_id = ModId(random_base62(8));
|
||||||
|
let mut retry_count = 0;
|
||||||
|
|
||||||
|
//Check if ID is unique
|
||||||
|
loop {
|
||||||
|
let filter = doc! { "_id": mod_id.0 };
|
||||||
|
|
||||||
|
if mods.find(filter, None).await?.next().await.is_some() {
|
||||||
|
mod_id = ModId(random_base62(8));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
retry_count += 1;
|
||||||
|
if retry_count > 20 {
|
||||||
|
return Err(CreateError::RandomIdError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut created_versions: Vec<Version> = vec![];
|
||||||
|
|
||||||
|
let mut mod_create_data: Option<ModCreateData> = None;
|
||||||
|
let mut icon_url = "".to_string();
|
||||||
|
|
||||||
|
let mut current_file_index = 0;
|
||||||
|
while let Some(item) = payload.next().await {
|
||||||
|
let mut field: Field = item.map_err(CreateError::MultipartError)?;
|
||||||
|
let content_disposition = field.content_disposition().ok_or_else(|| {
|
||||||
|
CreateError::MissingValueError("Missing content disposition!".to_string())
|
||||||
|
})?;
|
||||||
|
let name = content_disposition
|
||||||
|
.get_name()
|
||||||
|
.ok_or_else(|| CreateError::MissingValueError("Missing content name!".to_string()))?;
|
||||||
|
|
||||||
|
while let Some(chunk) = field.next().await {
|
||||||
|
let data = &chunk.map_err(CreateError::MultipartError)?;
|
||||||
|
|
||||||
|
if name == "data" {
|
||||||
|
mod_create_data = Some(serde_json::from_slice(&data)?);
|
||||||
|
} else {
|
||||||
|
let file_name = content_disposition.get_filename().ok_or_else(|| {
|
||||||
|
CreateError::MissingValueError("Missing content file name!".to_string())
|
||||||
|
})?;
|
||||||
|
let file_extension = String::from_utf8(
|
||||||
|
content_disposition
|
||||||
|
.get_filename_ext()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CreateError::MissingValueError("Missing file extension!".to_string())
|
||||||
|
})?
|
||||||
|
.clone()
|
||||||
|
.value,
|
||||||
|
)
|
||||||
|
.map_err(CreateError::InvalidUtf8Input)?;
|
||||||
|
|
||||||
|
if let Some(create_data) = &mod_create_data {
|
||||||
|
if name == "icon" {
|
||||||
|
if let Some(ext) = get_image_content_type(file_extension) {
|
||||||
|
let upload_data = upload_file(
|
||||||
|
upload_url.get_ref().clone(),
|
||||||
|
ext,
|
||||||
|
format!("mods/icons/{}/{}", mod_id.0, file_name),
|
||||||
|
data.to_vec(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
icon_url = format!("{}/{}", cdn_url, upload_data.file_name);
|
||||||
|
} else {
|
||||||
|
panic!("Invalid Icon Format!");
|
||||||
|
}
|
||||||
|
} else if &*file_extension == "jar" {
|
||||||
|
let initial_version_data = create_data
|
||||||
|
.initial_versions
|
||||||
|
.iter()
|
||||||
|
.position(|x| x.file_indexes.contains(¤t_file_index));
|
||||||
|
|
||||||
|
if let Some(version_data_index) = initial_version_data {
|
||||||
|
let version_data = create_data
|
||||||
|
.initial_versions
|
||||||
|
.get(version_data_index)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CreateError::MissingValueError(
|
||||||
|
"Missing file extension!".to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut created_version_filter = created_versions
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|x| x.number == version_data.version_number);
|
||||||
|
|
||||||
|
match created_version_filter.next() {
|
||||||
|
Some(created_version) => {
|
||||||
|
let upload_data = upload_file(
|
||||||
|
upload_url.get_ref().clone(),
|
||||||
|
"application/java-archive".to_string(),
|
||||||
|
format!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
create_data.mod_namespace.replace(".", "/"),
|
||||||
|
version_data.version_number,
|
||||||
|
file_name
|
||||||
|
),
|
||||||
|
data.to_vec(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
created_version.files.push(VersionFile {
|
||||||
|
game_versions: version_data
|
||||||
|
.game_versions
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.0)
|
||||||
|
.collect(),
|
||||||
|
hashes: vec![FileHash {
|
||||||
|
algorithm: "sha1".to_string(),
|
||||||
|
hash: upload_data.content_sha1,
|
||||||
|
}],
|
||||||
|
url: format!("{}/{}", cdn_url, upload_data.file_name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
//Check if ID is unique
|
||||||
|
let mut version_id = VersionId(random_base62(8));
|
||||||
|
retry_count = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let filter = doc! { "_id": version_id.0 };
|
||||||
|
|
||||||
|
if versions.find(filter, None).await?.next().await.is_some()
|
||||||
|
{
|
||||||
|
version_id = VersionId(random_base62(8));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
retry_count += 1;
|
||||||
|
if retry_count > 20 {
|
||||||
|
return Err(CreateError::RandomIdError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body_url = format!(
|
||||||
|
"data/{}/changelogs/{}/body.md",
|
||||||
|
mod_id.0, version_id.0
|
||||||
|
);
|
||||||
|
|
||||||
|
upload_file(
|
||||||
|
upload_url.get_ref().clone(),
|
||||||
|
"text/plain".to_string(),
|
||||||
|
body_url.clone(),
|
||||||
|
version_data.version_body.into_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let upload_data = upload_file(
|
||||||
|
upload_url.get_ref().clone(),
|
||||||
|
"application/java-archive".to_string(),
|
||||||
|
format!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
create_data.mod_namespace.replace(".", "/"),
|
||||||
|
version_data.version_number,
|
||||||
|
file_name
|
||||||
|
),
|
||||||
|
data.to_vec(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let version = Version {
|
||||||
|
version_id: version_id.0 as i32,
|
||||||
|
mod_id: mod_id.0 as i32,
|
||||||
|
name: version_data.version_title,
|
||||||
|
number: version_data.version_number.clone(),
|
||||||
|
changelog_url: Some(format!("{}/{}", cdn_url, body_url)),
|
||||||
|
date_published: Utc::now().to_rfc2822(),
|
||||||
|
downloads: 0,
|
||||||
|
version_type: version_data.version_type.to_string(),
|
||||||
|
files: vec![VersionFile {
|
||||||
|
game_versions: version_data
|
||||||
|
.game_versions
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.0)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
hashes: vec![FileHash {
|
||||||
|
algorithm: "sha1".to_string(),
|
||||||
|
hash: upload_data.content_sha1,
|
||||||
|
}],
|
||||||
|
url: format!("{}/{}", cdn_url, upload_data.file_name),
|
||||||
|
}],
|
||||||
|
dependencies: version_data
|
||||||
|
.dependencies
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.0 as i32)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
game_versions: vec![],
|
||||||
|
loaders: vec![],
|
||||||
|
};
|
||||||
|
//TODO: Malware scan + file validation
|
||||||
|
|
||||||
|
created_versions.push(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_file_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for version in &created_versions {
|
||||||
|
let serialized_version = serde_json::to_string(&version)?;
|
||||||
|
let document = Bson::from(serialized_version)
|
||||||
|
.as_document()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CreateError::MissingValueError(
|
||||||
|
"No document present for database entry!".to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
versions.insert_one(document, None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(create_data) = mod_create_data {
|
||||||
|
let body_url = format!("data/{}/body.md", mod_id.0);
|
||||||
|
|
||||||
|
upload_file(
|
||||||
|
upload_url.get_ref().clone(),
|
||||||
|
"text/plain".to_string(),
|
||||||
|
body_url.clone(),
|
||||||
|
create_data.mod_body.into_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let created_mod: Mod = Mod {
|
||||||
|
id: mod_id.0 as i32,
|
||||||
|
team: Team {
|
||||||
|
id: random_base62(8) as i32,
|
||||||
|
members: create_data
|
||||||
|
.team_members
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| crate::database::models::TeamMember {
|
||||||
|
user_id: x.user_id.0 as i32,
|
||||||
|
name: x.name,
|
||||||
|
role: x.role,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
title: create_data.mod_name,
|
||||||
|
icon_url: Some(icon_url),
|
||||||
|
description: create_data.mod_description,
|
||||||
|
body_url: format!("{}/{}", cdn_url, body_url),
|
||||||
|
published: Utc::now().to_rfc2822(),
|
||||||
|
downloads: 0,
|
||||||
|
categories: create_data.categories,
|
||||||
|
version_ids: created_versions
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.version_id as i32)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
issues_url: create_data.issues_url,
|
||||||
|
source_url: create_data.source_url,
|
||||||
|
wiki_url: create_data.wiki_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized_mod = serde_json::to_string(&created_mod)?;
|
||||||
|
let document = Bson::from(serialized_mod)
|
||||||
|
.as_document()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CreateError::MissingValueError(
|
||||||
|
"No document present for database entry!".to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
mods.insert_one(document, None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_image_content_type(extension: String) -> Option<String> {
|
||||||
|
let content_type = match &*extension {
|
||||||
|
"bmp" => "image/bmp",
|
||||||
|
"gif" => "image/gif",
|
||||||
|
"jpeg" | "jpg" | "jpe" => "image/jpeg",
|
||||||
|
"png" => "image/png",
|
||||||
|
"svg" | "svgz" => "image/svg+xml",
|
||||||
|
"webp" => "image/webp",
|
||||||
|
"rgb" => "image/x-rgb",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if content_type != "" {
|
||||||
|
Some(content_type.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use crate::models::mods::SearchRequest;
|
|||||||
use crate::search::{search_for_mod, SearchError};
|
use crate::search::{search_for_mod, SearchError};
|
||||||
use actix_web::{get, web, HttpResponse};
|
use actix_web::{get, web, HttpResponse};
|
||||||
|
|
||||||
#[get("api/v1/mods")]
|
#[get("api/v1/mod")]
|
||||||
pub async fn mod_search(
|
pub async fn mod_search(
|
||||||
web::Query(info): web::Query<SearchRequest>,
|
web::Query(info): web::Query<SearchRequest>,
|
||||||
) -> Result<HttpResponse, SearchError> {
|
) -> Result<HttpResponse, SearchError> {
|
||||||
|
|||||||
@@ -41,16 +41,22 @@ pub async fn index_local(client: mongodb::Client) -> Result<Vec<SearchMod>, Inde
|
|||||||
mod_game_versions.append(&mut version.game_versions);
|
mod_game_versions.append(&mut version.game_versions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut icon_url = "".to_string();
|
||||||
|
|
||||||
|
if let Some(url) = result.icon_url {
|
||||||
|
icon_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
docs_to_add.push(SearchMod {
|
docs_to_add.push(SearchMod {
|
||||||
mod_id: result.id,
|
mod_id: result.id,
|
||||||
author: result.author,
|
author: "".to_string(),
|
||||||
title: result.title,
|
title: result.title,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
keywords: result.categories,
|
keywords: result.categories,
|
||||||
versions: mod_game_versions,
|
versions: mod_game_versions,
|
||||||
downloads: result.downloads,
|
downloads: result.downloads,
|
||||||
page_url: "".to_string(),
|
page_url: "".to_string(),
|
||||||
icon_url: result.icon_path,
|
icon_url,
|
||||||
author_url: "".to_string(),
|
author_url: "".to_string(),
|
||||||
date_created: "".to_string(),
|
date_created: "".to_string(),
|
||||||
created: 0,
|
created: 0,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub async fn index_mods(db: mongodb::Client) -> Result<(), IndexingError> {
|
|||||||
.parse()
|
.parse()
|
||||||
.expect("`INDEX_CURSEFORGE` is not a boolean.")
|
.expect("`INDEX_CURSEFORGE` is not a boolean.")
|
||||||
{
|
{
|
||||||
docs_to_add.append(&mut index_curseforge(1, 400000).await?);
|
docs_to_add.append(&mut index_curseforge(1, 400_000).await?);
|
||||||
}
|
}
|
||||||
//Write Indexes
|
//Write Indexes
|
||||||
//Relevance Index
|
//Relevance Index
|
||||||
|
|||||||
Reference in New Issue
Block a user