diff --git a/Cargo.lock b/Cargo.lock index 879900d7..00dc8f33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3116,6 +3116,7 @@ name = "onelauncher_migration" version = "0.2.0-alpha.2" dependencies = [ "async-trait", + "chrono", "sea-orm-migration", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 673465c0..48eadd5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,22 @@ [workspace] -resolver = "2" # virtual workspace resolver so we don't need a root package members = [ "packages/core", "packages/entity", "packages/gamemode", "packages/macro", "packages/migration", - "packages/serde_json_path_to_error" + "packages/serde_json_path_to_error", ] +resolver = "2" # virtual workspace resolver so we don't need a root package [workspace.package] -license = "GPL-3.0-only" -edition = "2024" -repository = "https://github.com/Polyfrost/OneLauncher" -documentation = "https://docs.polyfrost.org/onelauncher" -readme = "README.md" -homepage = "https://polyfrost.org/projects/onelauncher" authors = [ "Polyfrost", "Polyfrost Contributors" ] +documentation = "https://docs.polyfrost.org/onelauncher" +edition = "2024" +homepage = "https://polyfrost.org/projects/onelauncher" +license = "GPL-3.0-only" +readme = "README.md" +repository = "https://github.com/Polyfrost/OneLauncher" version = "0.2.0-alpha.2" [workspace.dependencies] @@ -34,16 +34,16 @@ tauri = { version = "2.0", features = [ "tracing", "specta", "uuid", - "protocol-asset" + "protocol-asset", ] } tauri-build = { version = "2.0" } -tauri-plugin-shell = { version = "2.0" } -tauri-plugin-window-state = { version = "2.0" } -tauri-plugin-single-instance = { version = "2.0" } tauri-plugin-clipboard-manager = { version = "2.0" } -tauri-plugin-updater = { version = "2.0" } -tauri-plugin-dialog = { version = "2.0" } tauri-plugin-deep-link = { version = "2.0" } +tauri-plugin-dialog = { version = "2.0" } +tauri-plugin-shell = { version = "2.0" } +tauri-plugin-single-instance = { version = "2.0" } +tauri-plugin-updater = { version = "2.0" } +tauri-plugin-window-state = { version = "2.0" } # core interpulse = { version = "=1.2.2", features = [ "specta" ] } @@ -56,10 +56,10 @@ specta = { version = "2.0.0-rc.20", features = [ "serde_json", "function", "derive", - "time" + "time", ] } -tauri-specta = { version = "2.0.0-rc.20", features = [ "javascript", "typescript", "derive" ] } specta-typescript = { version = "=0.0.7" } +tauri-specta = { version = "2.0.0-rc.20", features = [ "javascript", "typescript", "derive" ] } # security iota_stronghold = { version = "2.1" } @@ -67,27 +67,28 @@ zeroize = { version = "1.8", features = [ "zeroize_derive" ] } # logging tracing = { version = "0.1" } -tracing-subscriber = { version = "0.3", features = [ "chrono", "env-filter" ] } -tracing-error = { version = "0.2" } tracing-appender = { version = "0.2" } +tracing-error = { version = "0.2" } +tracing-subscriber = { version = "0.3", features = [ "chrono", "env-filter" ] } # serialization & text +base64 = { version = "0.22" } +byteorder = { version = "1.5.0" } +chrono = { version = "0.4", features = [ "serde" ] } +nom = { version = "7.1" } +p256 = { version = "0.13.2", features = [ "ecdsa" ] } +rand = { version = "0.8.5" } +regex = { version = "1.10" } serde = { version = "1.0", features = [ "derive" ] } serde_json = { version = "1.0" } serde_with = { version = "=3.12.0", features = [ "chrono_0_4", "json", "macros" ] } -regex = { version = "1.10" } -url = { version = "2.5.2" } strum = { version = "0.26", features = [ "derive" ] } -chrono = { version = "0.4", features = [ "serde" ] } -rand = { version = "0.8.5" } -byteorder = { version = "1.5.0" } -base64 = { version = "0.22" } -p256 = { version = "0.13.2", features = [ "ecdsa" ] } -uuid = { version = "1.10", features = [ "serde", "v4" ] } toml = { version = "0.8" } -nom = { version = "7.1" } +url = { version = "2.5.2" } +uuid = { version = "1.10", features = [ "serde", "v4" ] } # network +async-tungstenite = { version = "0.28", features = [ "tokio-runtime", "tokio-rustls-webpki-roots" ] } reqwest = { version = "0.12", default-features = false, features = [ "json", "stream", @@ -99,33 +100,32 @@ reqwest = { version = "0.12", default-features = false, features = [ "macos-system-configuration", "deflate", "gzip", - "brotli" + "brotli", ] } -async-tungstenite = { version = "0.28", features = [ "tokio-runtime", "tokio-rustls-webpki-roots" ] } webbrowser = { version = "1.0" } # io -tokio = { version = "1.40", features = [ "full" ] } -tokio-util = { version = "0.7", features = [ "compat" ] } -tokio-stream = { version = "0.1.16", features = [ "fs" ] } +async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] } +async_zip = { version = "0.0.17", features = [ "full" ] } +bytes = { version = "1.7" } +dirs = { version = "5.0" } +dunce = { version = "1.0.5" } +flate2 = { version = "1.0" } notify = { version = "6.1", default-features = false } notify-debouncer-mini = { version = "0.4", default-features = false } -tempfile = { version = "3.19" } -dunce = { version = "1.0.5" } -zip = { version = "2.2.0" } -dirs = { version = "5.0" } -async_zip = { version = "0.0.17", features = [ "full" ] } -async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] } -flate2 = { version = "1.0" } -bytes = { version = "1.7" } sanitize-filename = { version = "0.6" } +tempfile = { version = "3.19" } +tokio = { version = "1.40", features = [ "full" ] } +tokio-stream = { version = "0.1.16", features = [ "fs" ] } +tokio-util = { version = "0.7", features = [ "compat" ] } +zip = { version = "2.2.0" } # crypto digest = { version = "0.10" } -sha1 = { version = "0.10" } -sha2 = { version = "0.10" } md-5 = { version = "0.10" } murmur2 = { version = "0.1.0" } +sha1 = { version = "0.10" } +sha2 = { version = "0.10" } # db sea-orm = { version = "1.1", features = [ @@ -134,85 +134,91 @@ sea-orm = { version = "1.1", features = [ "macros", "with-chrono", "with-json", - "with-uuid" + "with-uuid", +] } +sea-orm-migration = { version = "1.1", features = [ + "sqlx-sqlite", + "runtime-tokio-rustls", + "with-chrono", + "with-json", + "with-uuid", ] } -sea-orm-migration = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls" ] } # utils -thiserror = { version = "1.0" } anyhow = { version = "1.0" } -sysinfo = { version = "0.31", features = [ "serde" ] } +async-trait = { version = "0.1" } +cc = { version = "1.1" } discord-rich-presence = { version = "0.2" } -indicatif = { version = "0.17" } futures = { version = "0.3" } futures-lite = { version = "2.6" } -async-trait = { version = "0.1" } -whoami = { version = "1.5" } -cc = { version = "1.1" } +indicatif = { version = "0.17" } libc = { version = "0.2" } merge = { version = "0.2" } +sysinfo = { version = "0.31", features = [ "serde" ] } +thiserror = { version = "1.0" } +whoami = { version = "1.5" } # macro -syn = { version = "2" } -quote = { version = "1" } paste = { version = "1" } +quote = { version = "1" } +syn = { version = "2" } [workspace.lints.clippy] all = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } -correctness = { level = "warn", priority = -1 } -perf = { level = "warn", priority = -1 } -style = { level = "warn", priority = -1 } -suspicious = { level = "warn", priority = -1 } -complexity = { level = "warn", priority = -1 } -nursery = { level = "warn", priority = -1 } -unnecessary_cast = "warn" cast_lossless = "warn" +cast_precision_loss = "allow" +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } dbg_macro = "warn" deprecated_cfg_attr = "warn" -separated_literal_suffix = "warn" -missing_errors_doc = "allow" future_not_send = "allow" -module_name_repetitions = "allow" -struct_field_names = "allow" -cast_precision_loss = "allow" +missing_errors_doc = "allow" missing_panics_doc = "allow" -significant_drop_tightening = "allow" # tmp +module_name_repetitions = "allow" needless_return = "allow" +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +separated_literal_suffix = "warn" +significant_drop_tightening = "allow" # tmp +struct_field_names = "allow" +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } +unnecessary_cast = "warn" [workspace.lints.rust] -unused_qualifications = "warn" +deprecated = "warn" +deprecated_in_future = "forbid" rust_2018_idioms = { level = "warn", priority = -1 } trivial_casts = "warn" trivial_numeric_casts = "warn" unused_allocation = "warn" unused_import_braces = "warn" -deprecated = "warn" -deprecated_in_future = "forbid" unused_must_use = "deny" +unused_qualifications = "warn" [profile.dev] # add debug symbols/logic and optimize for reduced build time (parallel) -debug = true # add debug info -opt-level = 0 # reduce optimizations -strip = "none" # don't remove debug symbols -incremental = true # enable incremental compilation -split-debuginfo = "unpacked" # unpack debug info -lto = false # disable link time optimizations, reduce compile time codegen-units = 256 # make parallel incremental builds faster -rpath = false # disable rpath +debug = true # add debug info +incremental = true # enable incremental compilation +lto = false # disable link time optimizations, reduce compile time +opt-level = 0 # reduce optimizations panic = "unwind" # add unwind panic logic because some deps require it +rpath = false # disable rpath +split-debuginfo = "unpacked" # unpack debug info +strip = "none" # don't remove debug symbols # add debug symbols/logic and optimize for reduced build time (parallel) [profile.dev-debug] -inherits = "dev" # extend the dev profile -split-debuginfo = "none" # don't split debug info -opt-level = 0 # reduce optimizations -debug = "full" # add full debug info -strip = "none" # don't remove debug symbols -lto = "off" # disable link time optimizations, reduce compile time codegen-units = 256 # make parallel incremental builds faster +debug = "full" # add full debug info incremental = true # enable incremental compilation +inherits = "dev" # extend the dev profile +lto = "off" # disable link time optimizations, reduce compile time +opt-level = 0 # reduce optimizations +split-debuginfo = "none" # don't split debug info +strip = "none" # don't remove debug symbols # set the settings for build scripts and proc-macros. [profile.dev.build-override] @@ -220,20 +226,20 @@ opt-level = 3 # optimize build # set the default for dependencies, except workspace members. [profile.dev.package."*"] -opt-level = 3 # optimize build incremental = false # disable incremental compilation +opt-level = 3 # optimize build # set the default for dependencies, except workspace members. [profile.dev-debug.package."*"] -inherits = "dev" # extend the dev profile -opt-level = 3 # optimize build debug = "full" # add full debug info incremental = false # disable incremental compilation +inherits = "dev" # extend the dev profile +opt-level = 3 # optimize build # optimize for speed and reduce size for release [profile.release] -panic = "abort" # strip panic cleanup logic codegen-units = 1 # compile crate one after another for better optimization lto = true # enable link time optimization opt-level = "s" # optimize binary +panic = "abort" # strip panic cleanup logic strip = true # remove debug symbols diff --git a/packages/core/examples/launch.rs b/packages/core/examples/launch.rs index d3d23f82..7614e7ac 100644 --- a/packages/core/examples/launch.rs +++ b/packages/core/examples/launch.rs @@ -53,7 +53,7 @@ pub async fn main() -> LauncherResult<()> { let cluster_name = "Launchable"; let path = dirs.clusters_dir().join(cluster_name); - let mut cluster = if let Some(cluster) = api::cluster::dao::get_cluster_by_path(&path).await? { + let mut cluster = if let Some(cluster) = api::cluster::dao::get_cluster_by_folder_name(&path).await? { cluster } else { api::cluster::create_cluster(cluster_name, "1.20.4", GameLoader::Fabric, None, None).await? @@ -66,7 +66,7 @@ pub async fn main() -> LauncherResult<()> { Ok(cluster) }).await?; - println!("Using cluster: {} at '{}'", cluster.name, cluster.path); + println!("Using cluster: {} at '{}'", cluster.name, cluster.folder_name); // Launch the game let process = api::game::launch::launch_minecraft(&mut cluster, creds, None).await?; diff --git a/packages/core/examples/packages.rs b/packages/core/examples/packages.rs new file mode 100644 index 00000000..68b8eaf3 --- /dev/null +++ b/packages/core/examples/packages.rs @@ -0,0 +1,39 @@ +use onelauncher_core::{api::{cluster, packages::{self, download_package, provider::ProviderExt}, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::{CoreOptions, Dirs}}; +use onelauncher_entity::{loader::GameLoader, package::Provider}; + +#[tokio::main] +pub async fn main() -> LauncherResult<()> { + initialize_core(CoreOptions::default(), ProxyDynamic::default()).await?; + + let dirs = Dirs::get().await?; + + // Here we create the cluster + let cluster_name = "Chatting Profile"; + let path = dirs.clusters_dir().join(cluster_name); + + let cluster = if let Some(cluster) = cluster::dao::get_cluster_by_folder_name(&path).await? { + cluster + } else { + cluster::create_cluster(cluster_name, "1.8.9", GameLoader::Forge, None, None).await? + }; + + // Fetch our package and its versions + let provider = Provider::Modrinth; + let package = &provider.get("chatting").await?; + let versions = provider.get_versions(package.version_ids.as_slice()).await?; + + let version = versions.iter().find(|v| v.mc_versions.contains(&"1.8.9".to_string())).expect("Version supporting 1.8.9 not found"); + + // We can now download the package + let model = download_package(package, version, None).await?; + + // Link the package to the cluster + packages::link_package(&model, &cluster, None).await?; + + // Check if the package is hard linked + let cluster_dir = dirs.clusters_dir().join(cluster.folder_name.clone()); + let pkg_path = cluster_dir.join(model.package_type.folder_name()).join(model.file_name.as_str()); + assert!(tokio::fs::metadata(&pkg_path).await.is_ok(), "Package not found in cluster directory"); + + Ok(()) +} \ No newline at end of file diff --git a/packages/core/src/api/cluster/dao.rs b/packages/core/src/api/cluster/dao.rs index af494462..ad2d7bf7 100644 --- a/packages/core/src/api/cluster/dao.rs +++ b/packages/core/src/api/cluster/dao.rs @@ -10,7 +10,7 @@ pub type ClusterId = i64; /// Inserts a cluster in the database. pub async fn insert_cluster( name: &str, - path: &str, + folder_name: &str, mc_version: &str, mc_loader: GameLoader, mc_loader_version: Option<&str>, @@ -21,7 +21,7 @@ pub async fn insert_cluster( let model = clusters::ActiveModel { name: Set(name.to_string()), - path: Set(path.to_string()), + folder_name: Set(folder_name.to_string()), mc_version: Set(mc_version.to_string()), mc_loader: Set(mc_loader), mc_loader_version: Set(mc_loader_version.map(String::from)), @@ -96,12 +96,12 @@ pub async fn get_cluster_by_id(id: ClusterId) -> LauncherResult LauncherResult> { +pub async fn get_cluster_by_folder_name(folder_name: &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())) + .filter(clusters::Column::FolderName.eq(folder_name.to_string_lossy().to_string())) .one(db) .await?; diff --git a/packages/core/src/api/cluster/mod.rs b/packages/core/src/api/cluster/mod.rs index 0549a560..024d33a8 100644 --- a/packages/core/src/api/cluster/mod.rs +++ b/packages/core/src/api/cluster/mod.rs @@ -55,15 +55,17 @@ pub async fn create_cluster( let cluster_dir = Dirs::get_clusters_dir().await?; // Get the directory for the cluster - let mut path = cluster_dir.join(&name); + let mut folder_name = name.clone(); + let mut path = cluster_dir.join(&folder_name); // Folder name conflict resolution if path.exists() { let mut which = 1; loop { - let new_name = format!("{name} ({which})"); + let new_name = format!("{folder_name} ({which})"); path = cluster_dir.join(&new_name); if !path.exists() { + folder_name = new_name; break; } which += 1; @@ -84,7 +86,7 @@ pub async fn create_cluster( // Finally add the cluster to the database dao::insert_cluster( name.as_str(), - path.to_string_lossy().into_owned().as_str(), + &folder_name, mc_version, mc_loader, mc_loader_version, @@ -253,7 +255,7 @@ async fn run_forge_processors( client => cluster.mc_version.clone(), server => ""; "ROOT": - client => cluster.path.clone(), + client => dirs.clusters_dir().join(cluster.folder_name.clone()).to_string_lossy(), server => ""; "LIBRARY_DIR": client => libraries.to_string_lossy(), @@ -314,7 +316,7 @@ pub async fn update_playtime(id: ClusterId, duration: i64) -> LauncherResult LauncherResult> { let clusters = dao::get_all_clusters().await?; let mut missing_ids = Vec::new(); - let mut checked_paths = HashSet::new(); + let mut checked_folders = HashSet::new(); + let dirs = Dirs::get().await?; for cluster in clusters { if sync_cluster(&cluster) @@ -40,14 +41,14 @@ pub async fn sync_clusters() -> LauncherResult> { { missing_ids.push(cluster.id); - let path = cluster.path.clone(); + let path = cluster.folder_name.clone(); send_error!( "cluster {} is missing from the filesystem", path.clone() ); } - checked_paths.insert(PathBuf::from(cluster.path)); + checked_folders.insert(dirs.clusters_dir().join(cluster.folder_name.clone())); } // Check for clusters in the filesystem that are not in the database @@ -58,7 +59,7 @@ pub async fn sync_clusters() -> LauncherResult> { 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) { + if !path.is_dir() || checked_folders.contains(&path) { // Skip if it's not a directory or if we've already checked it continue; } @@ -79,7 +80,7 @@ pub async fn sync_clusters() -> LauncherResult> { /// 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()); + let path = Dirs::get_clusters_dir().await?.join(cluster.folder_name.clone()); if !path.exists() { return Ok(SyncAction::MissingFs); } @@ -104,7 +105,7 @@ pub async fn sync_cluster_by_id(id: ClusterId) -> LauncherResult { /// 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 { + let Some(cluster) = dao::get_cluster_by_folder_name(path).await? else { tracing::warn!("cluster with path {path:?} not found in database"); return Ok(SyncAction::MissingDb); }; diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index b1b146d1..7a78f59b 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -5,12 +5,10 @@ use chrono::Utc; use discord_rich_presence::activity::Timestamps; use merge::Merge; use onelauncher_entity::prelude::model::*; -use sea_orm::ActiveValue::Set; use tokio::process::Command; use tokio::sync::RwLock; -use crate::api::cluster::dao::update_cluster; -use crate::api::cluster::{ClusterError, prepare_cluster}; +use crate::api::cluster::{prepare_cluster, update_playtime, ClusterError}; use crate::api::game::{arguments, log, metadata}; use crate::api::java::{self, JavaError}; use crate::api::setting_profiles::dao::get_profile_by_name; @@ -41,6 +39,7 @@ pub async fn launch_minecraft( } let state = State::get().await?; + let dirs = Dirs::get().await?; if !state.settings.read().await.allow_parallel_running_clusters { let running = state.processes.has_running(cluster.id).await; @@ -49,7 +48,7 @@ pub async fn launch_minecraft( } } - let cwd = &io::canonicalize(&cluster.path)?; + let cwd = &io::canonicalize(dirs.clusters_dir().join(cluster.folder_name.clone()))?; let metadata = state.metadata.read().await; let versions = &metadata.get_vanilla()?.versions; @@ -85,7 +84,6 @@ pub async fn launch_minecraft( let java_path = PathBuf::from(&java.absolute_path); java::check_java_runtime(&java_path, false).await?; - let dirs = Dirs::get().await?; let client_jar_path = dirs .versions_dir() .join(&version_name) @@ -187,19 +185,14 @@ pub async fn launch_minecraft( io::write(&options_path, options_string).await?; } - let start_time = Utc::now().timestamp(); - update_cluster(cluster, async |mut cluster| { - cluster.last_played = Set(Some(start_time)); - Ok(cluster) - }) - .await?; + update_playtime(cluster.id, 0).await?; let censors = log::create_censors(&creds); if let Some(discord) = &state.rpc { discord.set_message( &format!("Playing {}", cluster.name), - Some(Timestamps::new().start(start_time)), + Some(Timestamps::new().start(Utc::now().timestamp())), ).await; } diff --git a/packages/core/src/api/packages/dao.rs b/packages/core/src/api/packages/dao.rs new file mode 100644 index 00000000..94195019 --- /dev/null +++ b/packages/core/src/api/packages/dao.rs @@ -0,0 +1,165 @@ +use onelauncher_entity::loader::GameLoader; +use onelauncher_entity::package::PackageType; +use onelauncher_entity::{cluster_packages, clusters, packages}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, +}; + +use crate::error::{DaoError, LauncherResult}; +use crate::store::State; + +use super::PackageError; + +pub type PackageId = String; + +/// Inserts a new package into the database. +pub async fn insert_package(package: packages::ActiveModel) -> LauncherResult { + if package + .package_type + .try_as_ref() + .is_some_and(|p| p == &PackageType::ModPack) + { + return Err(PackageError::IsModPack.into()); + } + + let state = State::get().await?; + let db = &state.db; + + Ok(package.insert(db).await?) +} + +/// Links a package to a cluster in database. +pub async fn link_package_to_cluster( + package: &packages::Model, + cluster: &clusters::Model, +) -> LauncherResult<()> { + let state = State::get().await?; + let db = &state.db; + + let cluster_package = cluster_packages::Model { + cluster_id: cluster.id, + package_hash: package.hash.clone(), + } + .into_active_model(); + + cluster_packages::Entity::insert(cluster_package) + .exec(db) + .await?; + + Ok(()) +} + +/// Retrieves a clusters linked packages from the database. +pub async fn get_linked_packages( + cluster: &clusters::Model, +) -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + let linked_packages = cluster_packages::Entity::find() + .filter(cluster_packages::Column::ClusterId.eq(cluster.id)) + .find_with_related(packages::Entity) + .all(db) + .await? + .into_iter() + .flat_map(|(_cluster_packages, packages)| packages) + .collect::>(); + + Ok(linked_packages) +} + +/// Is the package linked to the cluster? +pub async fn is_package_linked_to_cluster( + package: &packages::Model, + cluster: &clusters::Model, +) -> LauncherResult { + let state = State::get().await?; + let db = &state.db; + + let linked_packages = cluster_packages::Entity::find() + .filter(cluster_packages::Column::ClusterId.eq(cluster.id)) + .filter(cluster_packages::Column::PackageHash.eq(package.hash.clone())) + .one(db) + .await?; + + Ok(linked_packages.is_some()) +} + +/// Retrieves a package by its hash from the database. +pub async fn get_package_by_hash(hash: PackageId) -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + Ok(packages::Entity::find_by_id(hash).one(db).await?) +} + +/// Retrieves all packages that match the package type, loader and version(s) in the database. +pub async fn get_packages_for( + package_type: PackageType, + loader: GameLoader, + versions: Vec, +) -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + Ok(packages::Entity::find() + .filter(packages::Column::PackageType.eq(package_type)) + .filter(packages::Column::McLoader.eq(loader)) + .all(db) + .await? + .into_iter() + .filter(|p| { + let mc_versions = p.mc_versions.clone(); + versions.iter().any(|v| mc_versions.0.contains(v)) + }) + .collect()) +} + +/// Updates an existing package in the database. +pub async fn update_package_by_hash(id: PackageId, block: B) -> LauncherResult +where + B: AsyncFnOnce(packages::ActiveModel) -> LauncherResult, +{ + let state = State::get().await?; + let db = &state.db; + + let model = get_package_by_hash(id).await?.ok_or(DaoError::NotFound)?; + let model = block(model.into_active_model()).await?; + let model = model.update(db).await?; + + Ok(model) +} + +/// Updates an existing package in the database. +pub async fn update_package( + package: &mut packages::Model, + block: B, +) -> LauncherResult<&mut packages::Model> +where + B: AsyncFnOnce(packages::ActiveModel) -> LauncherResult, +{ + let state = State::get().await?; + let db = &state.db; + + let model = package.clone().into_active_model(); + let model = block(model).await?; + let model = model.update(db).await?; + + *package = model; + + Ok(package) +} + +/// Deletes a package by its hash from the **database**. +pub async fn delete_package_by_id(id: PackageId) -> LauncherResult<()> { + let state = State::get().await?; + let db = &state.db; + + let deleted = packages::Entity::delete_by_id(id).exec(db).await?; + + if deleted.rows_affected == 0 { + return Err(DaoError::NotFound.into()); + } + + Ok(()) +} diff --git a/packages/core/src/api/packages/data.rs b/packages/core/src/api/packages/data.rs index e1689419..c50c5cb0 100644 --- a/packages/core/src/api/packages/data.rs +++ b/packages/core/src/api/packages/data.rs @@ -40,8 +40,8 @@ pub struct SearchQuery { #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ManagedPackage { + pub id: String, pub provider: Provider, - pub provider_id: String, pub package_type: PackageType, pub name: String, pub short_desc: String, diff --git a/packages/core/src/api/packages/mod.rs b/packages/core/src/api/packages/mod.rs index 2992a718..6ea9e81c 100644 --- a/packages/core/src/api/packages/mod.rs +++ b/packages/core/src/api/packages/mod.rs @@ -1,5 +1,180 @@ +use data::{ManagedPackage, ManagedVersion}; +use onelauncher_entity::icon::Icon; +use onelauncher_entity::package::PackageType; +use onelauncher_entity::{clusters, packages}; +use reqwest::Method; + +use crate::error::{LauncherError, LauncherResult}; +use crate::store::Dirs; +use crate::store::ingress::SubIngress; +use crate::utils::crypto::HashAlgorithm; +use crate::utils::io::IOError; +use crate::utils::{http, icon, io}; + +pub mod dao; pub mod data; pub mod provider; #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; + +#[derive(Debug, thiserror::Error)] +pub enum PackageError { + #[error("no file marked for download")] + NoPrimaryFile, + #[error("package is a modpack, it must be handled differently")] + IsModPack, + #[error(transparent)] + Incompatible(#[from] IncompatiblePackageType), +} + +#[derive(Debug, thiserror::Error)] +pub enum IncompatiblePackageType { + #[error("package is not compatible with the current version")] + McVersion, + #[error("package is not compatible with the current loader")] + Loader, +} + +/// Downloads a package and registers it in the database from the given provider. +#[tracing::instrument(level = "debug", skip_all)] +pub async fn download_package( + package: &ManagedPackage, + version: &ManagedVersion, + ingress: Option>, +) -> LauncherResult { + // Modpacks are downloaded and managed differently. + if package.package_type == PackageType::ModPack { + return Err(PackageError::IsModPack.into()); + } + + tracing::debug!( + "downloading package '{}' version '{}' from '{}'", + package.name, + version.version_id, + package.provider + ); + + // Check if the package is already downloaded + + let primary_file = version + .files + .iter() + .find(|f| f.primary) + .ok_or(PackageError::NoPrimaryFile)?; + + if let Some(model) = dao::get_package_by_hash(primary_file.sha1.clone()).await? { + tracing::debug!("package is already downloaded"); + return Ok(model); + } + + let dir = Dirs::get_package_dir(&package.package_type, &package.provider, &package.id).await?; + + io::create_dir_all(&dir).await?; + + let dest = join_package_file(dir, &version.version_id, primary_file.file_name.as_str()); + + http::download_advanced( + Method::GET, + &primary_file.url, + dest, + None, + None, + Some((HashAlgorithm::Sha1, primary_file.sha1.as_str())), + ingress.as_ref(), + ) + .await?; + + let icon = if let Some(icon_url) = &package.icon_url { + if let Some(icon) = Icon::try_from_url(url::Url::parse(icon_url)?) { + Some(icon::cache_icon(&icon).await?) + } else { + None + } + } else { + None + }; + + let model = packages::Model { + hash: primary_file.sha1.clone(), + display_name: package.name.clone(), + display_version: version.display_version.clone(), + file_name: primary_file.file_name.clone(), + version_id: version.version_id.clone(), + published_at: version.published, + provider: package.provider.clone(), + icon, + package_id: package.id.clone(), + mc_loader: version.loaders.clone().into(), + mc_versions: version.mc_versions.clone().into(), + package_type: package.package_type.clone(), + }; + + dao::insert_package(model.into()).await +} + +/// Links a package to a cluster on the file system and in database. +/// * `skip_compatibility` - Checks whether the `mc_loader` and `mc_version` is compatible. Default is false +#[tracing::instrument(level = "debug", skip_all)] +pub async fn link_package( + package: &packages::Model, + cluster: &clusters::Model, + skip_compatibility: Option, +) -> LauncherResult<()> { + if package.package_type == PackageType::ModPack { + return Err(PackageError::IsModPack.into()); + } + + tracing::debug!( + "linking package '{}' version '{}' to cluster '{}'", + package.display_name, + package.display_version, + cluster.name + ); + + tracing::trace!("checking if package is already linked to cluster"); + if dao::is_package_linked_to_cluster(package, cluster).await? { + tracing::debug!("package is already linked to cluster"); + return Ok(()); + } + + tracing::trace!("checking compatibility of package with cluster"); + if !skip_compatibility.unwrap_or(false) { + if !package.mc_loader.iter().any(|v| cluster.mc_loader.compatible_with(v)) { + return Err(LauncherError::from(PackageError::from(IncompatiblePackageType::Loader))); + } + + if !package.mc_versions.iter().any(|v| cluster.mc_version.contains(v)) { + return Err(LauncherError::from(PackageError::from(IncompatiblePackageType::McVersion))); + } + } + + let dirs = Dirs::get().await?; + let cluster_dir = dirs.clusters_dir().join(cluster.folder_name.clone()); + + tracing::trace!("hard linking package to cluster"); + let src_pkg = path_from_model(dirs, package); + let dest_dir = cluster_dir.join(package.package_type.folder_name()); + io::create_dir_all(&dest_dir).await?; + + let dest_pkg = dest_dir.join(package.file_name.as_str()); + tokio::fs::hard_link(src_pkg, dest_pkg).await.map_err(IOError::from)?; + + dao::link_package_to_cluster(package, cluster).await?; + + tracing::debug!( + "linked package '{}' version '{}' to cluster '{}'", + package.display_name, + package.display_version, + cluster.name + ); + Ok(()) +} + +fn path_from_model(dirs: &Dirs, model: &packages::Model) -> std::path::PathBuf { + join_package_file(dirs.package_dir(&model.package_type, &model.provider, &model.package_id), &model.version_id, model.file_name.as_str()) +} + +fn join_package_file(path: impl AsRef, version_id: &str, file_name: &str) -> std::path::PathBuf { + path.as_ref().join(format!("{version_id}-{file_name}")) +} \ No newline at end of file diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth.rs index 17a5a9de..3e1468fc 100644 --- a/packages/core/src/api/packages/provider/modrinth.rs +++ b/packages/core/src/api/packages/provider/modrinth.rs @@ -308,7 +308,7 @@ impl From for ManagedPackage { fn from(value: ModrinthPackage) -> Self { Self { provider: Provider::Modrinth, - provider_id: value.id, + id: value.id, package_type: value.project_type, name: value.title, short_desc: value.description, diff --git a/packages/core/src/api/packages/tests.rs b/packages/core/src/api/packages/tests.rs index 427314cf..06595ec6 100644 --- a/packages/core/src/api/packages/tests.rs +++ b/packages/core/src/api/packages/tests.rs @@ -1,6 +1,7 @@ + use onelauncher_entity::package::Provider; -use crate::{api::packages::provider::ProviderExt, error::LauncherResult, store::{Core, CoreOptions}}; +use crate::{api::packages::{self, provider::ProviderExt}, error::LauncherResult, store::{Core, CoreOptions, Dirs}}; #[tokio::test] pub async fn test_get_provider() -> LauncherResult<()> { @@ -10,7 +11,7 @@ pub async fn test_get_provider() -> LauncherResult<()> { let res = provider.get("oneconfig").await?; - assert_eq!(res.provider_id, "AibBIVmj"); + assert_eq!(res.id, "AibBIVmj"); Ok(()) } @@ -41,4 +42,26 @@ pub async fn test_get_versions() -> LauncherResult<()> { Ok(()) } +#[tokio::test] +pub async fn test_download_chatting() -> LauncherResult<()> { + Core::initialize(CoreOptions::default()).await?; + + let provider = Provider::Modrinth; + + let pkg = provider.get("chatting").await?; + let ver_id = pkg.version_ids.first().expect("No version found"); + let versions = provider.get_versions(&[ver_id.clone()]).await?; + let ver = versions.first().expect("No version found"); + + let db_model = packages::download_package(&pkg, ver, None).await; + assert!(db_model.is_ok_and(|m| m.display_name == pkg.name)); + + let dir = Dirs::get_package_dir(&pkg.package_type, &provider, &pkg.id).await?; + let dest = dir.join(&ver.files.first().unwrap().file_name); + + assert!(dest.exists(), "File does not exist: {dest:?}"); + + Ok(()) +} + // TODO: Add more tests diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index 065d8ac8..82a7a351 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -21,6 +21,8 @@ pub enum LauncherError { #[error(transparent)] ProcessError(#[from] crate::store::processes::ProcessError), #[error(transparent)] + PackageError(#[from] crate::api::packages::PackageError), + #[error(transparent)] DaoError(#[from] DaoError), #[error(transparent)] diff --git a/packages/core/src/store/dirs.rs b/packages/core/src/store/dirs.rs index b0eb90e6..62b1dce9 100644 --- a/packages/core/src/store/dirs.rs +++ b/packages/core/src/store/dirs.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use onelauncher_entity::package::{PackageType, Provider}; use tokio::sync::OnceCell; use crate::{utils, LauncherResult}; @@ -46,19 +47,19 @@ impl Dirs { } macro_rules! dirs_impl { - ($($name:ident => |$self:ident| $path:expr),*$(,)?) => { + ($($name:ident = |$self:ident$(, $param:ident : $type:ty )*| $path:expr),*$(,)?) => { paste::paste! { impl Dirs { $( #[must_use] - pub fn $name(&self) -> PathBuf { + pub fn $name(&self $(, $param: $type)*) -> PathBuf { let $self = self; $path } - pub async fn []() -> LauncherResult { + pub async fn []($($param: $type),*) -> LauncherResult { Dirs::get().await.map(|$self| { - $self.$name() + $self.$name($($param),*) }) } )* @@ -68,22 +69,30 @@ macro_rules! dirs_impl { } dirs_impl! { - db_file => |this| this.base_dir.join("user_data.db"), - settings_file => |this| this.base_dir.join("settings.json"), - auth_file => |this| this.base_dir.join("auth.json"), + db_file = |this| this.base_dir.join("user_data.db"), + settings_file = |this| this.base_dir.join("settings.json"), + auth_file = |this| this.base_dir.join("auth.json"), - launcher_logs_dir => |this| this.base_dir.join("logs"), - clusters_dir => |this| this.base_dir.join("clusters"), - metadata_dir => |this| this.base_dir.join("metadata"), + launcher_logs_dir = |this| this.base_dir.join("logs"), + clusters_dir = |this| this.base_dir.join("clusters"), + metadata_dir = |this| this.base_dir.join("metadata"), - versions_dir => |this| this.metadata_dir().join("versions"), - libraries_dir => |this| this.metadata_dir().join("libraries"), - natives_dir => |this| this.metadata_dir().join("natives"), - java_dir => |this| this.metadata_dir().join("java"), - caches_dir => |this| this.metadata_dir().join("caches"), + versions_dir = |this| this.metadata_dir().join("versions"), + libraries_dir = |this| this.metadata_dir().join("libraries"), + natives_dir = |this| this.metadata_dir().join("natives"), + java_dir = |this| this.metadata_dir().join("java"), + caches_dir = |this| this.metadata_dir().join("caches"), + packages_dir = |this| this.metadata_dir().join("packages"), - assets_dir => |this| this.metadata_dir().join("assets"), - legacy_assets_dir => |this| this.metadata_dir().join("resources"), - assets_index_dir => |this| this.assets_dir().join("indexes"), - assets_object_dir => |this| this.assets_dir().join("objects"), -} \ No newline at end of file + assets_dir = |this| this.metadata_dir().join("assets"), + legacy_assets_dir = |this| this.metadata_dir().join("resources"), + assets_index_dir = |this| this.assets_dir().join("indexes"), + assets_object_dir = |this| this.assets_dir().join("objects"), + + package_dir = |this, package_type: &PackageType, provider: &Provider, project_id: &str| { + this.packages_dir() + .join(package_type.folder_name()) + .join(provider.name()) + .join(project_id) + } +} diff --git a/packages/core/src/utils/http.rs b/packages/core/src/utils/http.rs index 31a1d4f4..3ddd9784 100644 --- a/packages/core/src/utils/http.rs +++ b/packages/core/src/utils/http.rs @@ -139,7 +139,7 @@ pub async fn fetch_advanced( } } - tracing::debug!("finished downloading {url}"); + tracing::debug!("finished fetching {url}"); Ok(bytes) } diff --git a/packages/core/src/utils/icon.rs b/packages/core/src/utils/icon.rs new file mode 100644 index 00000000..15b28b43 --- /dev/null +++ b/packages/core/src/utils/icon.rs @@ -0,0 +1,40 @@ +use onelauncher_entity::icon::{Icon, IconType}; +use reqwest::Method; + +use crate::{error::LauncherResult, store::Dirs}; + +use super::{crypto::HashAlgorithm, http, io}; + +/// Caches a icon on the filesystem. Name is the equivalent to the sha1 hash of the icon. +pub async fn cache_icon(icon: &Icon) -> LauncherResult { + Ok(match icon.get_type() { + IconType::Cache => icon.clone(), + IconType::Path => { + let bytes = io::read(&**icon).await?; + cache_icon_bytes(&bytes).await? + }, + IconType::Url => { + let bytes = http::fetch(Method::GET, icon).await?; + cache_icon_bytes(&bytes).await? + }, + IconType::Unknown => { + return Err(anyhow::anyhow!( + "icon type is unknown, cannot cache icon: {}", + icon + ).into()); + }, + }) +} + +/// Caches a icon bytes on the filesystem. Name is the equivalent to the sha1 hash of the icon. +pub async fn cache_icon_bytes(bytes: &[u8]) -> LauncherResult { + let hash = HashAlgorithm::Sha1.hash(bytes).await?; + + let dir = Dirs::get_caches_dir().await?.join(&hash[0..2]); + io::create_dir_all(&dir).await?; + + let path = dir.join(&hash); + io::write(&path, bytes).await?; + + Ok(Icon::from_hash(hash)) +} diff --git a/packages/core/src/utils/mod.rs b/packages/core/src/utils/mod.rs index 48a06a26..54a197b5 100644 --- a/packages/core/src/utils/mod.rs +++ b/packages/core/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod crypto; pub mod os_ext; pub mod http; +pub mod icon; pub mod io; pub mod pagination; \ No newline at end of file diff --git a/packages/entity/src/domain/icon.rs b/packages/entity/src/domain/icon.rs index cbd51cb3..89054a67 100644 --- a/packages/entity/src/domain/icon.rs +++ b/packages/entity/src/domain/icon.rs @@ -13,8 +13,25 @@ impl Nullable for Icon { } impl Icon { - pub fn new(icon: String) -> Self { - Self(icon) + pub fn from_hash(hash: String) -> Self { + Self(format!("cache://{hash}")) + } + + pub fn try_from_path(path: impl AsRef) -> Option { + let path = path.as_ref(); + if !path.is_absolute() { + return None; + } + + Some(Self(format!("file://{}", path.display()))) + } + + pub fn try_from_url(url: url::Url) -> Option { + if url.scheme() != "http" && url.scheme() != "https" { + return None; + } + + Some(Self(url.as_str().to_string())) } pub fn get_type(&self) -> IconType { @@ -22,29 +39,36 @@ impl Icon { IconType::Path } else if self.is_url() { IconType::Url + } else if self.is_cached() { + IconType::Cache } else { - IconType::Stored + IconType::Unknown } } + /// Returns true if the icon is a path to a file. pub fn is_path(&self) -> bool { self.0.starts_with("file://") } + /// Returns true if the icon is a HTTP URL. 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() + /// Returns true if the icon is a cached icon. Relative to the cache directory. + pub fn is_cached(&self) -> bool { + self.0.starts_with("cache://") } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum IconType { + #[default] + Unknown, Path, Url, - Stored, + Cache, } impl Display for IconType { @@ -63,7 +87,7 @@ impl Deref for Icon { impl From for Icon { fn from(s: String) -> Self { - Self::new(s) + Self(s) } } @@ -71,7 +95,7 @@ impl FromStr for Icon { type Err = (); fn from_str(s: &str) -> Result { - Ok(Self::new(s.to_string())) + Ok(Self(s.to_string())) } } diff --git a/packages/entity/src/domain/loader.rs b/packages/entity/src/domain/loader.rs index ed62c5db..14dc3322 100644 --- a/packages/entity/src/domain/loader.rs +++ b/packages/entity/src/domain/loader.rs @@ -44,6 +44,17 @@ impl GameLoader { } } + pub fn compatible_with(&self, other: &Self) -> bool { + match self { + Self::Vanilla => matches!(other, Self::Vanilla), + Self::Forge => matches!(other, Self::Forge), + Self::NeoForge => matches!(other, Self::NeoForge), + Self::Quilt => matches!(other, Self::Quilt | Self::Fabric), + Self::Fabric => matches!(other, Self::Fabric), + Self::LegacyFabric => matches!(other, Self::LegacyFabric), + } + } + } impl TryFrom for GameLoader { diff --git a/packages/entity/src/domain/mod.rs b/packages/entity/src/domain/mod.rs index 8a70f6b1..c8fa2206 100644 --- a/packages/entity/src/domain/mod.rs +++ b/packages/entity/src/domain/mod.rs @@ -4,3 +4,4 @@ pub mod loader; pub mod package; pub mod resolution; pub mod settings; +pub mod utility; diff --git a/packages/entity/src/domain/utility.rs b/packages/entity/src/domain/utility.rs new file mode 100644 index 00000000..1c5dacb8 --- /dev/null +++ b/packages/entity/src/domain/utility.rs @@ -0,0 +1,68 @@ +use std::ops::Deref; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct DbVec(pub Vec); + +impl Deref for DbVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for DbVec { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl sea_orm::TryGetableFromJson for DbVec where for<'de> T: Deserialize<'de> {} + +impl std::convert::From> for sea_orm::Value +where + DbVec: Serialize, +{ + fn from(source: DbVec) -> Self { + sea_orm::Value::Json( + serde_json::to_value(&source) + .ok() + .map(std::boxed::Box::new), + ) + } +} + +impl sea_orm::sea_query::ValueType for DbVec +where + DbVec: DeserializeOwned, +{ + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Json(Some(json)) => { + Ok(serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?) + } + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + stringify!(#ident).to_owned() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::Json + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::Json + } +} + +impl sea_orm::sea_query::Nullable for DbVec { + fn null() -> sea_orm::Value { + sea_orm::Value::Json(None) + } +} \ No newline at end of file diff --git a/packages/entity/src/entities/cluster_groups.rs b/packages/entity/src/entities/cluster_groups.rs index fec66c73..d62b79f7 100644 --- a/packages/entity/src/entities/cluster_groups.rs +++ b/packages/entity/src/entities/cluster_groups.rs @@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[sea_orm(primary_key)] pub id: i64, - #[sea_orm(column_type = "Text")] pub name: String, } diff --git a/packages/entity/src/entities/cluster_packages.rs b/packages/entity/src/entities/cluster_packages.rs index 1be43f7a..1a09e981 100644 --- a/packages/entity/src/entities/cluster_packages.rs +++ b/packages/entity/src/entities/cluster_packages.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub cluster_id: i64, - #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + #[sea_orm(primary_key, auto_increment = false)] pub package_hash: String, } diff --git a/packages/entity/src/entities/clusters.rs b/packages/entity/src/entities/clusters.rs index cd4ff0a3..50076337 100644 --- a/packages/entity/src/entities/clusters.rs +++ b/packages/entity/src/entities/clusters.rs @@ -9,25 +9,22 @@ use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader}; pub struct Model { #[sea_orm(primary_key)] pub id: i64, - #[sea_orm(column_type = "Text")] - pub path: String, + pub folder_name: String, pub stage: ClusterStage, - pub created_at: i64, + pub created_at: DateTimeUtc, pub group_id: Option, - #[sea_orm(column_type = "Text")] pub name: String, - #[sea_orm(column_type = "Text")] pub mc_version: String, pub mc_loader: GameLoader, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub mc_loader_version: Option, - pub last_played: Option, + pub last_played: Option, pub overall_played: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub icon_url: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub setting_profile_name: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub linked_pack_id: Option, pub linked_pack_version: Option, } diff --git a/packages/entity/src/entities/java_versions.rs b/packages/entity/src/entities/java_versions.rs index e52ceb72..b856bda9 100644 --- a/packages/entity/src/entities/java_versions.rs +++ b/packages/entity/src/entities/java_versions.rs @@ -10,13 +10,9 @@ pub struct Model { #[sea_orm(primary_key)] pub id: i64, pub major_version: u32, - #[sea_orm(column_type = "Text")] pub vendor_name: String, - #[sea_orm(column_type = "Text")] pub absolute_path: String, - #[sea_orm(column_type = "Text")] pub full_version: String, - #[sea_orm(column_type = "Text")] pub arch: String, } diff --git a/packages/entity/src/entities/packages.rs b/packages/entity/src/entities/packages.rs index d3d18c90..c1cb6447 100644 --- a/packages/entity/src/entities/packages.rs +++ b/packages/entity/src/entities/packages.rs @@ -3,30 +3,26 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::{icon::Icon, package::{PackageType, Provider}}; +use crate::{icon::Icon, loader::GameLoader, package::{PackageType, Provider}, utility::DbVec}; #[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")] + #[sea_orm(primary_key, auto_increment = false)] pub hash: String, - #[sea_orm(column_type = "Text")] pub file_name: String, - #[sea_orm(column_type = "Text")] + pub version_id: String, + pub published_at: DateTimeUtc, pub display_name: String, - #[sea_orm(column_type = "Text")] pub display_version: String, - pub project_type_id: PackageType, - pub provider_id: Provider, - #[sea_orm(column_type = "Text")] - pub provider_version: String, - #[sea_orm(column_type = "Text")] - pub mc_versions: String, - #[sea_orm(column_type = "Text")] - pub mc_loader: String, - #[sea_orm(column_type = "Text", nullable)] - pub icon_url: Option, + pub package_type: PackageType, + pub provider: Provider, + pub package_id: String, + pub mc_versions: DbVec, + pub mc_loader: DbVec, + #[sea_orm(nullable)] + pub icon: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/entity/src/entities/setting_profiles.rs b/packages/entity/src/entities/setting_profiles.rs index d8071af9..1c7e3aa3 100644 --- a/packages/entity/src/entities/setting_profiles.rs +++ b/packages/entity/src/entities/setting_profiles.rs @@ -15,21 +15,21 @@ pub struct Model { #[merge(skip)] pub name: String, pub java_id: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub res: Option, pub force_fullscreen: Option, pub mem_max: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub launch_args: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub launch_env: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub hook_pre: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub hook_wrapper: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub hook_post: Option, - #[sea_orm(column_type = "Text", nullable)] + #[sea_orm(nullable)] pub os_extra: Option } diff --git a/packages/migration/Cargo.toml b/packages/migration/Cargo.toml index ec7161a1..6244d409 100644 --- a/packages/migration/Cargo.toml +++ b/packages/migration/Cargo.toml @@ -19,3 +19,4 @@ path = "src/lib.rs" tokio = { workspace = true } async-trait = { workspace = true } sea-orm-migration = { workspace = true } +chrono = { workspace = true } diff --git a/packages/migration/src/init.sql b/packages/migration/src/init.sql deleted file mode 100644 index f358b051..00000000 --- a/packages/migration/src/init.sql +++ /dev/null @@ -1,82 +0,0 @@ -create table cluster_groups ( - id integer not null, - name text not null, - constraint cluster_groups_pk primary key (id autoincrement) -); - -create table java_versions ( - id integer not null, - major_version integer not null, - vendor_name text not null, - absolute_path text not null, - full_version text not null, - arch text not null, - constraint java_versions_pk primary key (id autoincrement) -); - -create index java_versions_major_version_idx on java_versions (major_version); - -create table packages ( - hash text not null, - file_name text not null, - display_name text not null, - display_version text not null, - project_type_id integer not null, - provider_id integer not null, - provider_version text not null, - mc_versions text not null, - mc_loader text not null, - icon_url text, - constraint packages_pk primary key (hash) -); - -create index packages_project_type_id_idx on packages (project_type_id); -create index packages_provider_id_idx on packages (provider_id); -create index packages_mc_loader_mc_versions_idx on packages (mc_loader, mc_versions); - -create table setting_profiles ( - name text not null, - java_id integer, - res json_text, - force_fullscreen boolean, - mem_max integer, - launch_args text, - launch_env text, - hook_pre text, - hook_wrapper text, - hook_post text, - os_extra json_text, - constraint setting_profiles_pk primary key (name), - constraint setting_profiles_java_versions_id_fk foreign key (java_id) references java_versions(id) -); - -create table clusters ( - id integer not null, - path text not null, - stage integer default (0) not null, - created_at integer default (unixepoch()) not null, - group_id integer, - name text not null, - mc_version text not null, - mc_loader integer not null, - mc_loader_version text, - last_played integer, - overall_played integer, - icon_url text, - setting_profile_name text, - linked_pack_id text, - linked_pack_version text, - constraint clusters_pk primary key (id autoincrement), - constraint clusters_cluster_groups_id_fk foreign key (group_id) references cluster_groups(id), - constraint clusters_setting_profiles_name_fk foreign key (setting_profile_name) references setting_profiles(name) -); - -create index clusters_path_idx on clusters (path); - -create table cluster_packages ( - cluster_id integer not null, - package_hash text not null, - constraint cluster_packages_pk primary key (cluster_id, package_hash), - constraint cluster_packages_clusters_id_fk foreign key (cluster_id) references clusters(id), - constraint cluster_packages_packages_hash_fk foreign key (package_hash) references packages(hash) -); diff --git a/packages/migration/src/m20220101_000001_create_table.rs b/packages/migration/src/m20220101_000001_create_table.rs index e3ba6a3b..6d07b263 100644 --- a/packages/migration/src/m20220101_000001_create_table.rs +++ b/packages/migration/src/m20220101_000001_create_table.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use sea_orm_migration::prelude::*; #[derive(DeriveMigrationName)] @@ -6,12 +7,270 @@ pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager.get_connection().execute_unprepared(include_str!("./init.sql")).await?; + manager + .create_table( + Table::create() + .table(ClusterGroups::Table) + .if_not_exists() + .col( + ColumnDef::new(ClusterGroups::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(ClusterGroups::Name).text().not_null()) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(JavaVersions::Table) + .if_not_exists() + .col( + ColumnDef::new(JavaVersions::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(JavaVersions::MajorVersion).integer().not_null()) + .col(ColumnDef::new(JavaVersions::VendorName).text().not_null()) + .col(ColumnDef::new(JavaVersions::AbsolutePath).text().not_null()) + .col(ColumnDef::new(JavaVersions::FullVersion).text().not_null()) + .col(ColumnDef::new(JavaVersions::Arch).text().not_null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("java_versions_major_version_idx") + .table(JavaVersions::Table) + .col(JavaVersions::MajorVersion) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(Packages::Table) + .if_not_exists() + .col(ColumnDef::new(Packages::Hash).text().not_null().primary_key()) + .col(ColumnDef::new(Packages::FileName).text().not_null()) + .col(ColumnDef::new(Packages::VersionId).text().not_null()) + .col(ColumnDef::new(Packages::PublishedAt).date_time().not_null()) + .col(ColumnDef::new(Packages::DisplayName).text().not_null()) + .col(ColumnDef::new(Packages::DisplayVersion).text().not_null()) + .col(ColumnDef::new(Packages::PackageType).integer().not_null()) + .col(ColumnDef::new(Packages::Provider).integer().not_null()) + .col(ColumnDef::new(Packages::PackageId).text().not_null()) + .col(ColumnDef::new(Packages::McVersions).json().not_null()) + .col(ColumnDef::new(Packages::McLoader).integer().not_null()) + .col(ColumnDef::new(Packages::Icon).text().null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("packages_mc_loader_mc_versions_idx") + .table(Packages::Table) + .col(Packages::PackageType) + .col(Packages::McLoader) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(SettingProfiles::Table) + .if_not_exists() + .col(ColumnDef::new(SettingProfiles::Name).text().not_null().primary_key()) + .col(ColumnDef::new(SettingProfiles::JavaId).integer().null()) + .col(ColumnDef::new(SettingProfiles::Res).json().null()) + .col(ColumnDef::new(SettingProfiles::ForceFullscreen).boolean().null()) + .col(ColumnDef::new(SettingProfiles::MemMax).integer().null()) + .col(ColumnDef::new(SettingProfiles::LaunchArgs).text().null()) + .col(ColumnDef::new(SettingProfiles::LaunchEnv).text().null()) + .col(ColumnDef::new(SettingProfiles::HookPre).text().null()) + .col(ColumnDef::new(SettingProfiles::HookWrapper).text().null()) + .col(ColumnDef::new(SettingProfiles::HookPost).text().null()) + .col(ColumnDef::new(SettingProfiles::OsExtra).json().null()) + .foreign_key( + ForeignKey::create() + .from(SettingProfiles::Table, SettingProfiles::JavaId) + .to(JavaVersions::Table, JavaVersions::Id), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(Clusters::Table) + .if_not_exists() + .col( + ColumnDef::new(Clusters::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Clusters::FolderName).text().not_null()) + .col(ColumnDef::new(Clusters::Stage).integer().not_null().default(0)) + .col(ColumnDef::new(Clusters::CreatedAt).date_time().not_null().default(Utc::now())) + .col(ColumnDef::new(Clusters::GroupId).integer().null()) + .col(ColumnDef::new(Clusters::Name).text().not_null()) + .col(ColumnDef::new(Clusters::McVersion).text().not_null()) + .col(ColumnDef::new(Clusters::McLoader).integer().not_null()) + .col(ColumnDef::new(Clusters::McLoaderVersion).text().null()) + .col(ColumnDef::new(Clusters::LastPlayed).date_time().null()) + .col(ColumnDef::new(Clusters::OverallPlayed).integer().null()) + .col(ColumnDef::new(Clusters::IconUrl).text().null()) + .col(ColumnDef::new(Clusters::SettingProfileName).text().null()) + .col(ColumnDef::new(Clusters::LinkedPackId).text().null()) + .col(ColumnDef::new(Clusters::LinkedPackVersion).text().null()) + .foreign_key( + ForeignKey::create() + .from(Clusters::Table, Clusters::GroupId) + .to(ClusterGroups::Table, ClusterGroups::Id), + ) + .foreign_key( + ForeignKey::create() + .from(Clusters::Table, Clusters::SettingProfileName) + .to(SettingProfiles::Table, SettingProfiles::Name), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("clusters_path_idx") + .table(Clusters::Table) + .col(Clusters::FolderName) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(ClusterPackages::Table) + .if_not_exists() + .col(ColumnDef::new(ClusterPackages::ClusterId).integer().not_null()) + .col(ColumnDef::new(ClusterPackages::PackageHash).text().not_null()) + .primary_key( + Index::create() + .col(ClusterPackages::ClusterId) + .col(ClusterPackages::PackageHash) + .primary(), + ) + .foreign_key( + ForeignKey::create() + .from(ClusterPackages::Table, ClusterPackages::ClusterId) + .to(Clusters::Table, Clusters::Id), + ) + .foreign_key( + ForeignKey::create() + .from(ClusterPackages::Table, ClusterPackages::PackageHash) + .to(Packages::Table, Packages::Hash), + ) + .to_owned(), + ) + .await?; Ok(()) } async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { - unreachable!("Down migration is not supported for init migration"); + Err(DbErr::Migration("no rollback available".to_owned())) } } + +#[derive(Iden)] +enum ClusterGroups { + Table, + Id, + Name, +} + +#[derive(Iden)] +enum JavaVersions { + Table, + Id, + MajorVersion, + VendorName, + AbsolutePath, + FullVersion, + Arch, +} + +#[derive(Iden)] +enum Packages { + Table, + Hash, + FileName, + VersionId, + PublishedAt, + DisplayName, + DisplayVersion, + PackageType, + Provider, + PackageId, + McVersions, + McLoader, + Icon, +} + +#[derive(Iden)] +enum SettingProfiles { + Table, + Name, + JavaId, + Res, + ForceFullscreen, + MemMax, + LaunchArgs, + LaunchEnv, + HookPre, + HookWrapper, + HookPost, + OsExtra, +} + +#[derive(Iden)] +enum Clusters { + Table, + Id, + FolderName, + Stage, + CreatedAt, + GroupId, + Name, + McVersion, + McLoader, + McLoaderVersion, + LastPlayed, + OverallPlayed, + IconUrl, + SettingProfileName, + LinkedPackId, + LinkedPackVersion, +} + +#[derive(Iden)] +enum ClusterPackages { + Table, + ClusterId, + PackageHash, +} \ No newline at end of file