From 2f7c26229ab1fac536f02b30643d9e362ad7a95f Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:12:03 +0200 Subject: [PATCH] feat: Cluster API --- Cargo.lock | 1 + package.json | 2 +- packages/core/examples/cluster.rs | 26 +++ packages/core/package.json | 3 +- packages/core/src/api/cluster/dao.rs | 120 +++++++++++ packages/core/src/api/cluster/mod.rs | 189 ++++++++++++++++++ packages/core/src/api/ingress.rs | 4 +- .../src/{store/java.rs => api/java/dao.rs} | 29 ++- .../core/src/api/{java.rs => java/mod.rs} | 30 +-- packages/core/src/api/mod.rs | 1 + packages/core/src/error.rs | 12 +- packages/core/src/store/dirs.rs | 6 + packages/core/src/store/mod.rs | 1 - packages/entity/Cargo.toml | 1 + packages/entity/src/domain/cluster_stage.rs | 14 ++ packages/entity/src/domain/icon.rs | 82 ++++++++ packages/entity/src/domain/loader.rs | 66 ++++++ packages/entity/src/domain/mod.rs | 3 + .../entity/src/entities/cluster_packages.rs | 48 +++++ packages/entity/src/entities/clusters.rs | 31 ++- packages/entity/src/entities/mod.rs | 1 + packages/entity/src/entities/packages.rs | 29 ++- packages/entity/src/entities/prelude.rs | 1 + packages/entity/src/lib.rs | 5 +- packages/migration/src/init.sql | 3 +- 25 files changed, 660 insertions(+), 48 deletions(-) create mode 100644 packages/core/examples/cluster.rs create mode 100644 packages/core/src/api/cluster/dao.rs create mode 100644 packages/core/src/api/cluster/mod.rs rename packages/core/src/{store/java.rs => api/java/dao.rs} (64%) rename packages/core/src/api/{java.rs => java/mod.rs} (80%) create mode 100644 packages/entity/src/domain/cluster_stage.rs create mode 100644 packages/entity/src/domain/icon.rs create mode 100644 packages/entity/src/domain/loader.rs create mode 100644 packages/entity/src/domain/mod.rs create mode 100644 packages/entity/src/entities/cluster_packages.rs diff --git a/Cargo.lock b/Cargo.lock index 04198c53..e339a1c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2993,6 +2993,7 @@ dependencies = [ "serde", "serde_json", "specta", + "url", "uuid 1.10.0", ] diff --git a/package.json b/package.json index f1bbbe23..c186a4a0 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "lint": "pnpm lint:es && pnpm lint:rs", "migrate": "sea-orm-cli migrate -d ./packages/migration", - "entity:gen": "sea-orm-cli generate entity -o packages/entity/src/entities --with-serde both --model-extra-attributes 'onelauncher_macro::specta'", + "entity:gen": "sea-orm-cli generate entity -o packages/entity/src/entities --with-serde both --model-extra-attributes 'onelauncher_macro::specta' -t", "frontend": "pnpm --filter @onelauncher/frontend -- ", "client": "pnpm --filter @onelauncher/client -- ", diff --git a/packages/core/examples/cluster.rs b/packages/core/examples/cluster.rs new file mode 100644 index 00000000..71b919c8 --- /dev/null +++ b/packages/core/examples/cluster.rs @@ -0,0 +1,26 @@ +use onelauncher_core::{api::{self, proxy::proxy_empty::ProxyEmpty}, error::LauncherResult, initialize_core, store::CoreOptions}; +use onelauncher_entity::loader::GameLoader; + +#[tokio::main] +async fn main() -> LauncherResult<()> { + initialize_core(CoreOptions::default(), ProxyEmpty::new()).await?; + + println!("syncing clusters"); + let missing = api::cluster::sync_clusters().await?; + if !missing.is_empty() { + println!("missing clusters: {missing:#?}"); + } + + println!("creating cluster"); + let cluster = api::cluster::create_cluster( + "Test Cluster", + "1.8.9", + GameLoader::Vanilla, + None, + None, + ).await?; + + println!("cluster created: {cluster:#?}"); + + Ok(()) +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 8d195c16..61a53f9e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,6 +12,7 @@ "sideEffects": false, "scripts": { "cargo": "cargo", - "binary": "cargo run --bin" + "binary": "cargo run --bin", + "example": "cargo run --example" } } diff --git a/packages/core/src/api/cluster/dao.rs b/packages/core/src/api/cluster/dao.rs new file mode 100644 index 00000000..80cd0dec --- /dev/null +++ b/packages/core/src/api/cluster/dao.rs @@ -0,0 +1,120 @@ +use std::path::Path; + +use onelauncher_entity::{cluster_groups, clusters, icon::Icon, loader::GameLoader}; +use sea_orm::{ActiveValue::Set, IntoActiveModel, prelude::*}; + +use crate::{error::{DaoError, LauncherResult}, store::State}; + +/// Inserts a cluster in the database. +pub async fn insert_cluster( + name: &str, + path: &str, + mc_version: &str, + mc_loader: GameLoader, + mc_loader_version: Option<&str>, + icon_url: Option, +) -> LauncherResult { + let state = State::get().await?; + let db = &state.db; + + let model = clusters::ActiveModel { + name: Set(name.to_string()), + path: Set(path.to_string()), + mc_version: Set(mc_version.to_string()), + mc_loader: Set(mc_loader), + mc_loader_version: Set(mc_loader_version.map(String::from)), + icon_url: Set(icon_url), + ..Default::default() + }; + + let model = model.insert(db).await?; + + Ok(model) +} + +/// Updates an existing cluster in the database. +pub async fn update_cluster_by_id( + id: i32, + block: B, +) -> LauncherResult +where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult { + let state = State::get().await?; + let db = &state.db; + + let model = get_cluster_by_id(id).await?.ok_or(DaoError::NotFound)?; + let model = block(model.into_active_model()).await?; + let model = model.update(db).await?; + + Ok(model) +} + +/// Deletes a cluster by its ID from the **database**. +pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> { + let state = State::get().await?; + let db = &state.db; + + let model = get_cluster_by_id(id).await?.ok_or(DaoError::NotFound)?; + model.delete(db).await?; + + Ok(()) +} + +/// Gets a cluster by its ID from the database. +pub async fn get_cluster_by_id(id: i32) -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + let model = clusters::Entity::find_by_id(id) + .one(db) + .await?; + + Ok(model) +} + +/// Gets a cluster by its path from the database. +pub async fn get_cluster_by_path(path: &Path) -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + let model = clusters::Entity::find() + .filter(clusters::Column::Path.eq(path.to_string_lossy().to_string())) + .one(db) + .await?; + + Ok(model) +} + +/// Gets all clusters from the database. +pub async fn get_all_clusters() -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + let clusters = clusters::Entity::find() + .all(db) + .await?; + + Ok(clusters) +} + +/// Gets all clusters grouped by their group from the database. +pub async fn get_clusters_grouped() -> LauncherResult)>> { + let state = State::get().await?; + let db = &state.db; + + let groups = cluster_groups::Entity::find() + .all(db) + .await?; + + let clusters = groups.load_many(clusters::Entity, db) + .await?; + + let mut grouped = Vec::new(); + + for (index, group) in groups.iter().enumerate() { + let group_clusters: Vec = clusters.get(index).cloned().unwrap_or_else(Vec::new); + + grouped.push((group.clone(), group_clusters)); + } + + Ok(grouped) +} diff --git a/packages/core/src/api/cluster/mod.rs b/packages/core/src/api/cluster/mod.rs new file mode 100644 index 00000000..50e28283 --- /dev/null +++ b/packages/core/src/api/cluster/mod.rs @@ -0,0 +1,189 @@ +use std::collections::HashSet; +use std::path::PathBuf; + +use onelauncher_entity::clusters; +use onelauncher_entity::icon::Icon; +use onelauncher_entity::loader::GameLoader; +use serde::Serialize; + +use crate::error::{DaoError, LauncherResult}; +use crate::send_error; +use crate::store::Dirs; +use crate::utils::io; + +pub mod dao; + +#[must_use] +pub fn sanitize_name(name: &str) -> String { + let mut name = name.to_string(); + name.retain(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | ' ' | '.' | '(' | ')')); + name +} + +#[tracing::instrument] +pub async fn create_cluster( + name: &str, + mc_version: &str, + mc_loader: GameLoader, + mc_loader_version: Option<&str>, + icon_url: Option, +) -> LauncherResult { + let name = sanitize_name(name); + let cluster_dir = Dirs::get().await?.clusters_dir(); + + // Get the directory for the cluster + let mut path = cluster_dir.join(&name); + + // Folder name conflict resolution + if path.exists() { + let mut which = 1; + loop { + let new_name = format!("{name} ({which})"); + path = cluster_dir.join(&new_name); + if !path.exists() { + break; + } + which += 1; + } + + tracing::warn!( + "collision while creating new cluster: {}, renaming to {}", + cluster_dir.display(), + path.display() + ); + } + + let result = async { + io::create_dir_all(&path).await?; + + tracing::info!("creating cluster at path {}", path.display()); + + // Finally add the cluster to the database + dao::insert_cluster( + name.as_str(), + path.to_string_lossy().into_owned().as_str(), + mc_version, + mc_loader, + mc_loader_version, + icon_url, + ) + .await + } + .await; + + match result { + Ok(result) => Ok(result), + Err(err) => { + tracing::error!("failed to create cluster: {}", err); + let _ = io::remove_dir_all(&path).await; + Err(err) + } + } +} + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub enum SyncAction { + Created, + Updated, + MissingDb, + MissingFs, + NoAction, +} + +impl SyncAction { + #[must_use] + pub const fn is_missing(self) -> bool { + matches!(self, Self::MissingDb | Self::MissingFs) + } +} + +/// Syncs all clusters in the database. Returns a list of cluster IDs that are missing. +#[tracing::instrument] +pub async fn sync_clusters() -> LauncherResult> { + let clusters = dao::get_all_clusters().await?; + + let mut missing_ids = Vec::new(); + let mut checked_paths = HashSet::new(); + + for cluster in clusters { + if sync_cluster(&cluster) + .await + .map(SyncAction::is_missing) + .unwrap_or(false) + { + missing_ids.push(cluster.id); + + let path = cluster.path.clone(); + send_error!( + "cluster {} is missing from the filesystem", + path.clone() + ); + } + + checked_paths.insert(PathBuf::from(cluster.path)); + } + + let cluster_dir = Dirs::get().await?.clusters_dir(); + let mut stream = io::read_dir(cluster_dir).await?; + while let Ok(Some(entry)) = stream.next_entry().await { + let path = entry.path(); + if !path.is_dir() || checked_paths.contains(&path) { + // Skip if it's not a directory or if we've already checked it + continue; + } + + // We have a directory that is not in the database + if let Err(err) = sync_from_fs_to_db(&path).await { + tracing::error!("failed to sync cluster from fs to db: {}", err); + }; + } + + Ok(missing_ids) +} + +/// Syncs the cluster with the database. +/// Checks if the cluster in the database exists and if the directory exists. +#[tracing::instrument] +pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult { + let path = PathBuf::from(cluster.path.clone()); + if !path.exists() { + return Ok(SyncAction::MissingFs); + } + + // TODO: sync packages + + Ok(SyncAction::NoAction) +} + +/// Syncs the cluster with the database. +/// Checks if the cluster in the database exists and if the directory exists. +#[tracing::instrument] +pub async fn sync_cluster_by_id(id: i32) -> LauncherResult { + let cluster = dao::get_cluster_by_id(id) + .await? + .ok_or(DaoError::NotFound)?; + + sync_cluster(&cluster).await +} + +/// Syncs the cluster with the database. +/// Checks if the cluster in the database exists and if the directory exists. +#[tracing::instrument] +pub async fn sync_cluster_by_path(path: &PathBuf) -> LauncherResult { + let Some(cluster) = dao::get_cluster_by_path(path).await? else { + tracing::warn!("cluster with path {path:?} not found in database"); + return Ok(SyncAction::MissingDb); + }; + + sync_cluster(&cluster).await +} + +#[tracing::instrument] +async fn sync_from_fs_to_db(path: &PathBuf) -> LauncherResult { + // TODO: Create database entry from what can be inferred from the directory + Err(anyhow::anyhow!( + "TODO: failed to sync cluster from fs to db: {}", + path.display() + ).into()) +} diff --git a/packages/core/src/api/ingress.rs b/packages/core/src/api/ingress.rs index f5425f29..715009cd 100644 --- a/packages/core/src/api/ingress.rs +++ b/packages/core/src/api/ingress.rs @@ -51,13 +51,15 @@ pub async fn send_ingress_opt( #[cfg(all(feature = "cli", test))] pub mod tests { + use crate::store::CoreOptions; + #[tokio::test] pub async fn create_and_update_ingress() -> crate::LauncherResult<()> { use std::time::Duration; use crate::{api::{ingress::{init_ingress, send_ingress}, proxy::proxy_cli::ProxyCli}, initialize_core, store::ingress::IngressType}; - initialize_core(ProxyCli::new()).await?; + initialize_core(CoreOptions::default(), ProxyCli::new()).await?; let id = init_ingress(IngressType::Download { file_name: "Some-Mod-1.8.9.jar".into() }, "This is a test message", 100.0).await?; tokio::time::sleep(Duration::from_millis(2350)).await; diff --git a/packages/core/src/store/java.rs b/packages/core/src/api/java/dao.rs similarity index 64% rename from packages/core/src/store/java.rs rename to packages/core/src/api/java/dao.rs index 5c92cf42..f2456a7b 100644 --- a/packages/core/src/store/java.rs +++ b/packages/core/src/api/java/dao.rs @@ -1,9 +1,29 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use onelauncher_entity::java_versions; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder}; +use sea_orm::{ActiveValue::Set, ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder}; -use crate::{api::java::{get_java_model, JavaInfo}, store::State, LauncherResult}; +use crate::{error::LauncherResult, store::State}; + +use super::{JavaError, JavaInfo}; + +/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`] +pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult { + let major_version = if info.java_version[0..2].eq("1.") { + info.java_version[2..3].parse::() + } else { + let split = info.java_version.split_once('.').unwrap_or_default(); + split.0.parse::() + }.map_err(JavaError::from)?; + + Ok(java_versions::ActiveModel { + absolute_path: Set(absolute_path.to_string_lossy().to_string()), + major_version: Set(major_version), + full_version: Set(info.java_version.clone()), + vendor_name: Set(info.java_vendor.clone()), + ..Default::default() + }) +} /// Returns all Java versions pub async fn get_java_all() -> LauncherResult> { @@ -58,5 +78,4 @@ pub async fn insert_java_many(java: Vec<(PathBuf, JavaInfo)>) -> LauncherResult< Ok(java_versions::Entity::insert_many(models) .exec_with_returning_many(db) .await?) -} - +} \ No newline at end of file diff --git a/packages/core/src/api/java.rs b/packages/core/src/api/java/mod.rs similarity index 80% rename from packages/core/src/api/java.rs rename to packages/core/src/api/java/mod.rs index a1e189c9..d7572c43 100644 --- a/packages/core/src/api/java.rs +++ b/packages/core/src/api/java/mod.rs @@ -1,12 +1,10 @@ -use std::{collections::HashMap, path::{Path, PathBuf}}; +use std::{collections::HashMap, path::PathBuf}; -use onelauncher_entity::java_versions; -use sea_orm::ActiveValue::Set; use serde::Serialize; -use crate::{error::LauncherResult, store::ingress::IngressType, utils::io}; +use crate::{api::ingress::{init_ingress, init_ingress_opt, send_ingress, send_ingress_opt}, error::LauncherResult, store::ingress::IngressType, utils::io}; -use super::ingress::{init_ingress, init_ingress_opt, send_ingress, send_ingress_opt}; +pub mod dao; #[derive(Debug, thiserror::Error)] pub enum JavaError { @@ -29,7 +27,7 @@ pub fn get_java_bin() -> PathBuf { } #[cfg(target_os = "windows")] { - PathBuf::new().join("bin").join("java.exe") + PathBuf::new().join("bin").join("javaw.exe") } #[cfg(not(any(target_os = "macos", target_os = "windows")))] { @@ -45,7 +43,7 @@ pub struct JavaInfo { pub java_vendor: String, } -const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../assets/java/JavaInfo.class"); +const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../../assets/java/JavaInfo.class"); /// Accepts a path to a java executable and returns the [`JavaInfo`] pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) -> LauncherResult { @@ -87,24 +85,6 @@ pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) -> }) } -/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`] -pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult { - let major_version = if info.java_version[0..2].eq("1.") { - info.java_version[2..3].parse::() - } else { - let split = info.java_version.split_once('.').unwrap_or_default(); - split.0.parse::() - }.map_err(JavaError::from)?; - - Ok(java_versions::ActiveModel { - absolute_path: Set(absolute_path.to_string_lossy().to_string()), - major_version: Set(major_version), - full_version: Set(info.java_version.clone()), - vendor_name: Set(info.java_vendor.clone()), - ..Default::default() - }) -} - /// Attempts to scan common paths for Java installations. /// /// **Can be heavy on I/O, don't run often!** diff --git a/packages/core/src/api/mod.rs b/packages/core/src/api/mod.rs index 69bcae7c..ef5fcc38 100644 --- a/packages/core/src/api/mod.rs +++ b/packages/core/src/api/mod.rs @@ -1,3 +1,4 @@ +pub mod cluster; pub mod ingress; pub mod proxy; pub mod java; \ No newline at end of file diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index 6ab08336..aed20bdc 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -12,6 +12,8 @@ pub enum LauncherError { CryptoError(#[from] crate::utils::crypto::CryptoError), #[error(transparent)] DiscordError(#[from] crate::store::discord::DiscordError), + #[error(transparent)] + DaoError(#[from] DaoError), #[error(transparent)] SerdeError(#[from] serde_json::Error), @@ -29,4 +31,12 @@ pub enum LauncherError { TauriError(#[from] tauri::Error), } -pub type LauncherResult = Result; \ No newline at end of file +pub type LauncherResult = Result; + +#[derive(thiserror::Error, Debug)] +pub enum DaoError { + #[error("entity was not found")] + NotFound, + #[error("entity already exists")] + AlreadyExists +} diff --git a/packages/core/src/store/dirs.rs b/packages/core/src/store/dirs.rs index 253384ec..14a0800a 100644 --- a/packages/core/src/store/dirs.rs +++ b/packages/core/src/store/dirs.rs @@ -58,6 +58,12 @@ impl Dirs { // --- GAME DIRECTORIES --- + /// Get the `config_dir/clusters` folder for the clusters. + #[must_use] + pub fn clusters_dir(&self) -> PathBuf { + self.base_dir().join("clusters") + } + /// Get the `config_dir/metadata` folder within the core config directory. #[inline] #[must_use] diff --git a/packages/core/src/store/mod.rs b/packages/core/src/store/mod.rs index 0b05c652..0181f555 100644 --- a/packages/core/src/store/mod.rs +++ b/packages/core/src/store/mod.rs @@ -2,7 +2,6 @@ pub(super) mod db; pub mod discord; pub mod ingress; pub mod proxy; -pub mod java; mod core_options; mod dirs; mod settings; diff --git a/packages/entity/Cargo.toml b/packages/entity/Cargo.toml index b00817ee..87b612fb 100644 --- a/packages/entity/Cargo.toml +++ b/packages/entity/Cargo.toml @@ -25,4 +25,5 @@ serde = { workspace = true } serde_json = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } +url = { workspace = true } specta = { workspace = true, optional = true } diff --git a/packages/entity/src/domain/cluster_stage.rs b/packages/entity/src/domain/cluster_stage.rs new file mode 100644 index 00000000..920951b6 --- /dev/null +++ b/packages/entity/src/domain/cluster_stage.rs @@ -0,0 +1,14 @@ +use sea_orm::{DeriveActiveEnum, EnumIter}; +use serde::{Deserialize, Serialize}; + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "u8", db_type = "Integer")] +#[serde(rename_all = "lowercase")] +pub enum ClusterStage { + #[default] + NotReady = 0, + Downloading = 1, + Repairing = 2, + Ready = 3, +} \ No newline at end of file diff --git a/packages/entity/src/domain/icon.rs b/packages/entity/src/domain/icon.rs new file mode 100644 index 00000000..cbd51cb3 --- /dev/null +++ b/packages/entity/src/domain/icon.rs @@ -0,0 +1,82 @@ +use std::{fmt::Display, ops::Deref, str::FromStr}; + +use sea_orm::{sea_query::Nullable, DeriveValueType}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)] +pub struct Icon(String); + +impl Nullable for Icon { + fn null() -> sea_orm::Value { + sea_orm::Value::String(None) + } +} + +impl Icon { + pub fn new(icon: String) -> Self { + Self(icon) + } + + pub fn get_type(&self) -> IconType { + if self.is_path() { + IconType::Path + } else if self.is_url() { + IconType::Url + } else { + IconType::Stored + } + } + + pub fn is_path(&self) -> bool { + self.0.starts_with("file://") + } + + pub fn is_url(&self) -> bool { + self.0.starts_with("http://") || self.0.starts_with("https://") + } + + pub fn is_stored(&self) -> bool { + !self.is_path() && !self.is_url() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum IconType { + Path, + Url, + Stored, +} + +impl Display for IconType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Deref for Icon { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Icon { + fn from(s: String) -> Self { + Self::new(s) + } +} + +impl FromStr for Icon { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(Self::new(s.to_string())) + } +} + +impl Display for Icon { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Icon(IconType::{}, \"{}\")", self.get_type(), self.0) + } +} diff --git a/packages/entity/src/domain/loader.rs b/packages/entity/src/domain/loader.rs new file mode 100644 index 00000000..aebe6129 --- /dev/null +++ b/packages/entity/src/domain/loader.rs @@ -0,0 +1,66 @@ +use std::{fmt::Display, str::FromStr}; + +use sea_orm::{DeriveActiveEnum, EnumIter}; +use serde::{Deserialize, Serialize}; + +#[onelauncher_macro::specta] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, DeriveActiveEnum)] +#[serde(rename_all = "lowercase")] +#[sea_orm(rs_type = "u8", db_type = "Integer")] +pub enum GameLoader { + #[default] + Vanilla = 0, + Unknown = 1, + Forge = 2, + NeoForge = 3, + Quilt = 4, + Fabric = 5, + LegacyFabric = 6, +} + +impl GameLoader { + + #[must_use] + pub const fn is_mod_loader(&self) -> bool { + matches!(self, Self::Forge | Self::NeoForge | Self::Quilt | Self::Fabric | Self::LegacyFabric) + } + +} + +impl From for GameLoader { + fn from(s: String) -> Self { + match s.to_lowercase().replace(['_', '.', ' '], "").as_str() { + "vanilla" => Self::Vanilla, + "forge" => Self::Forge, + "neoforge" => Self::NeoForge, + "quilt" => Self::Quilt, + "fabric" => Self::Fabric, + "legacyfabric" => Self::LegacyFabric, + _ => Self::Unknown, + } + } +} + +impl FromStr for GameLoader { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(Self::from(s.to_string())) + } +} + +impl Display for GameLoader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str( + match self { + Self::Vanilla => "Vanilla", + Self::Forge => "Forge", + Self::NeoForge => "NeoForge", + Self::Quilt => "Quilt", + Self::Fabric => "Fabric", + Self::LegacyFabric => "LegacyFabric", + Self::Unknown => "Unknown", + } + ) + } +} diff --git a/packages/entity/src/domain/mod.rs b/packages/entity/src/domain/mod.rs new file mode 100644 index 00000000..4a77aa5e --- /dev/null +++ b/packages/entity/src/domain/mod.rs @@ -0,0 +1,3 @@ +pub mod cluster_stage; +pub mod icon; +pub mod loader; \ No newline at end of file diff --git a/packages/entity/src/entities/cluster_packages.rs b/packages/entity/src/entities/cluster_packages.rs new file mode 100644 index 00000000..be0652ac --- /dev/null +++ b/packages/entity/src/entities/cluster_packages.rs @@ -0,0 +1,48 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "cluster_packages")] +#[onelauncher_macro::specta] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cluster_id: i32, + #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + pub package_hash: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::clusters::Entity", + from = "Column::ClusterId", + to = "super::clusters::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Clusters, + #[sea_orm( + belongs_to = "super::packages::Entity", + from = "Column::PackageHash", + to = "super::packages::Column::Hash", + on_update = "NoAction", + on_delete = "NoAction" + )] + Packages, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Clusters.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Packages.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/entity/src/entities/clusters.rs b/packages/entity/src/entities/clusters.rs index 163e5025..18ccdd65 100644 --- a/packages/entity/src/entities/clusters.rs +++ b/packages/entity/src/entities/clusters.rs @@ -1,14 +1,17 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5 - use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; +use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader}; + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "clusters")] #[onelauncher_macro::specta] pub struct Model { - #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text")] pub path: String, + pub stage: ClusterStage, pub created_at: i32, pub updated_at: i32, pub group_id: Option, @@ -16,14 +19,13 @@ pub struct Model { pub name: String, #[sea_orm(column_type = "Text")] pub mc_version: String, - #[sea_orm(column_type = "Text")] - pub mc_loader: String, + pub mc_loader: GameLoader, #[sea_orm(column_type = "Text", nullable)] pub mc_loader_version: Option, pub last_played: Option, pub overall_played: Option, #[sea_orm(column_type = "Text", nullable)] - pub icon_url: Option, + pub icon_url: Option, #[sea_orm(column_type = "Text", nullable)] pub setting_profile_name: Option, #[sea_orm(column_type = "Text", nullable)] @@ -41,6 +43,8 @@ pub enum Relation { on_delete = "NoAction" )] ClusterGroups, + #[sea_orm(has_many = "super::cluster_packages::Entity")] + ClusterPackages, #[sea_orm( belongs_to = "super::setting_profiles::Entity", from = "Column::SettingProfileName", @@ -57,10 +61,25 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::ClusterPackages.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::SettingProfiles.def() } } +impl Related for Entity { + fn to() -> RelationDef { + super::cluster_packages::Relation::Packages.def() + } + fn via() -> Option { + Some(super::cluster_packages::Relation::Clusters.def().rev()) + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/entity/src/entities/mod.rs b/packages/entity/src/entities/mod.rs index 2f91840a..60b6aa85 100644 --- a/packages/entity/src/entities/mod.rs +++ b/packages/entity/src/entities/mod.rs @@ -3,6 +3,7 @@ pub mod prelude; pub mod cluster_groups; +pub mod cluster_packages; pub mod clusters; pub mod java_versions; pub mod packages; diff --git a/packages/entity/src/entities/packages.rs b/packages/entity/src/entities/packages.rs index 4bcea7a3..48941707 100644 --- a/packages/entity/src/entities/packages.rs +++ b/packages/entity/src/entities/packages.rs @@ -3,17 +3,20 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; +use crate::icon::Icon; + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "packages")] #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + pub hash: String, + #[sea_orm(column_type = "Text")] pub file_name: String, #[sea_orm(column_type = "Text")] pub display_name: String, #[sea_orm(column_type = "Text")] pub display_version: String, - #[sea_orm(primary_key, auto_increment = false)] pub type_id: i32, pub provider_id: i32, #[sea_orm(column_type = "Text")] @@ -22,13 +25,29 @@ pub struct Model { pub mc_versions: String, #[sea_orm(column_type = "Text")] pub mc_loader: String, - #[sea_orm(column_type = "Text")] - pub hash: String, #[sea_orm(column_type = "Text", nullable)] - pub icon_url: Option, + pub icon_url: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::cluster_packages::Entity")] + ClusterPackages, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ClusterPackages.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::cluster_packages::Relation::Clusters.def() + } + fn via() -> Option { + Some(super::cluster_packages::Relation::Packages.def().rev()) + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/entity/src/entities/prelude.rs b/packages/entity/src/entities/prelude.rs index d7c9cef1..34bac74e 100644 --- a/packages/entity/src/entities/prelude.rs +++ b/packages/entity/src/entities/prelude.rs @@ -1,6 +1,7 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5 pub use super::cluster_groups::Entity as ClusterGroups; +pub use super::cluster_packages::Entity as ClusterPackages; pub use super::clusters::Entity as Clusters; pub use super::java_versions::Entity as JavaVersions; pub use super::packages::Entity as Packages; diff --git a/packages/entity/src/lib.rs b/packages/entity/src/lib.rs index 39561cf5..6c2d2707 100644 --- a/packages/entity/src/lib.rs +++ b/packages/entity/src/lib.rs @@ -1,2 +1,5 @@ mod entities; -pub use entities::*; \ No newline at end of file +mod domain; + +pub use entities::*; +pub use domain::*; \ No newline at end of file diff --git a/packages/migration/src/init.sql b/packages/migration/src/init.sql index b1f758f0..56c7eba7 100644 --- a/packages/migration/src/init.sql +++ b/packages/migration/src/init.sql @@ -48,12 +48,13 @@ create table setting_profiles ( create table clusters ( id integer not null, path text not null, + stage integer default (0) not null, created_at integer default (unixepoch()) not null, updated_at integer default (unixepoch()) not null, group_id integer, name text not null, mc_version text not null, - mc_loader text not null, + mc_loader integer not null, mc_loader_version text, last_played integer, overall_played integer,