feat: Cluster API
This commit is contained in:
parent
11d2ff43ed
commit
2f7c26229a
25 changed files with 660 additions and 48 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2993,6 +2993,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"url",
|
||||
"uuid 1.10.0",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -- ",
|
||||
|
|
|
|||
26
packages/core/examples/cluster.rs
Normal file
26
packages/core/examples/cluster.rs
Normal file
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"cargo": "cargo",
|
||||
"binary": "cargo run --bin"
|
||||
"binary": "cargo run --bin",
|
||||
"example": "cargo run --example"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
120
packages/core/src/api/cluster/dao.rs
Normal file
120
packages/core/src/api/cluster/dao.rs
Normal file
|
|
@ -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<Icon>,
|
||||
) -> LauncherResult<clusters::Model> {
|
||||
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<B>(
|
||||
id: i32,
|
||||
block: B,
|
||||
) -> LauncherResult<clusters::Model>
|
||||
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
|
||||
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<Option<clusters::Model>> {
|
||||
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<Option<clusters::Model>> {
|
||||
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<Vec<clusters::Model>> {
|
||||
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<Vec<(cluster_groups::Model, Vec<clusters::Model>)>> {
|
||||
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::Model> = clusters.get(index).cloned().unwrap_or_else(Vec::new);
|
||||
|
||||
grouped.push((group.clone(), group_clusters));
|
||||
}
|
||||
|
||||
Ok(grouped)
|
||||
}
|
||||
189
packages/core/src/api/cluster/mod.rs
Normal file
189
packages/core/src/api/cluster/mod.rs
Normal file
|
|
@ -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<Icon>,
|
||||
) -> LauncherResult<clusters::Model> {
|
||||
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<Vec<i32>> {
|
||||
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<SyncAction> {
|
||||
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<SyncAction> {
|
||||
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<SyncAction> {
|
||||
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<clusters::Model> {
|
||||
// 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())
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<java_versions::ActiveModel> {
|
||||
let major_version = if info.java_version[0..2].eq("1.") {
|
||||
info.java_version[2..3].parse::<i32>()
|
||||
} else {
|
||||
let split = info.java_version.split_once('.').unwrap_or_default();
|
||||
split.0.parse::<i32>()
|
||||
}.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<Vec<java_versions::Model>> {
|
||||
|
|
@ -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?)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<JavaInfo> {
|
||||
|
|
@ -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<java_versions::ActiveModel> {
|
||||
let major_version = if info.java_version[0..2].eq("1.") {
|
||||
info.java_version[2..3].parse::<i32>()
|
||||
} else {
|
||||
let split = info.java_version.split_once('.').unwrap_or_default();
|
||||
split.0.parse::<i32>()
|
||||
}.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!**
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod cluster;
|
||||
pub mod ingress;
|
||||
pub mod proxy;
|
||||
pub mod java;
|
||||
|
|
@ -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<T> = Result<T, LauncherError>;
|
||||
pub type LauncherResult<T> = Result<T, LauncherError>;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum DaoError {
|
||||
#[error("entity was not found")]
|
||||
NotFound,
|
||||
#[error("entity already exists")]
|
||||
AlreadyExists
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
14
packages/entity/src/domain/cluster_stage.rs
Normal file
14
packages/entity/src/domain/cluster_stage.rs
Normal file
|
|
@ -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,
|
||||
}
|
||||
82
packages/entity/src/domain/icon.rs
Normal file
82
packages/entity/src/domain/icon.rs
Normal file
|
|
@ -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<String> for Icon {
|
||||
fn from(s: String) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Icon {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
66
packages/entity/src/domain/loader.rs
Normal file
66
packages/entity/src/domain/loader.rs
Normal file
|
|
@ -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<String> 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<Self, Self::Err> {
|
||||
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",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
3
packages/entity/src/domain/mod.rs
Normal file
3
packages/entity/src/domain/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod cluster_stage;
|
||||
pub mod icon;
|
||||
pub mod loader;
|
||||
48
packages/entity/src/entities/cluster_packages.rs
Normal file
48
packages/entity/src/entities/cluster_packages.rs
Normal file
|
|
@ -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<super::clusters::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Clusters.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::packages::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Packages.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
@ -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<i32>,
|
||||
|
|
@ -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<String>,
|
||||
pub last_played: Option<i32>,
|
||||
pub overall_played: Option<i32>,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub icon_url: Option<String>,
|
||||
pub icon_url: Option<Icon>,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub setting_profile_name: Option<String>,
|
||||
#[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<super::cluster_groups::Entity> for Entity {
|
|||
}
|
||||
}
|
||||
|
||||
impl Related<super::cluster_packages::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ClusterPackages.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::setting_profiles::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SettingProfiles.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::packages::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::cluster_packages::Relation::Packages.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(super::cluster_packages::Relation::Clusters.def().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub icon_url: Option<Icon>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::cluster_packages::Entity")]
|
||||
ClusterPackages,
|
||||
}
|
||||
|
||||
impl Related<super::cluster_packages::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ClusterPackages.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::clusters::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::cluster_packages::Relation::Clusters.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(super::cluster_packages::Relation::Packages.def().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
mod entities;
|
||||
pub use entities::*;
|
||||
mod domain;
|
||||
|
||||
pub use entities::*;
|
||||
pub use domain::*;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue