From 1c7c52ee099e48f908ed956fdc58cddea7691c14 Mon Sep 17 00:00:00 2001 From: Jai A Date: Tue, 16 Jul 2024 23:29:02 -0700 Subject: [PATCH] barebones profiles --- .env | 1 + .gitignore | 3 + Cargo.lock | 258 +-- Cargo.toml | 3 + apps/app-playground/src/main.rs | 16 +- apps/app/Cargo.toml | 4 - packages/app-lib/Cargo.toml | 7 +- .../migrations/20240711194701_init.sql | 112 +- packages/app-lib/src/api/jre.rs | 6 +- packages/app-lib/src/api/logs.rs | 67 +- packages/app-lib/src/api/metadata.rs | 56 +- packages/app-lib/src/api/mod.rs | 16 +- .../app-lib/src/api/pack/import/atlauncher.rs | 64 +- .../app-lib/src/api/pack/import/curseforge.rs | 65 +- .../app-lib/src/api/pack/import/gdlauncher.rs | 43 +- packages/app-lib/src/api/pack/import/mmc.rs | 29 +- packages/app-lib/src/api/pack/import/mod.rs | 22 +- packages/app-lib/src/api/pack/install_from.rs | 83 +- .../app-lib/src/api/pack/install_mrpack.rs | 71 +- packages/app-lib/src/api/process.rs | 11 +- packages/app-lib/src/api/profile/create.rs | 258 +-- packages/app-lib/src/api/profile/mod.rs | 1347 +++++++------- packages/app-lib/src/api/profile/update.rs | 202 +- packages/app-lib/src/api/settings.rs | 235 +-- packages/app-lib/src/api/tags.rs | 66 +- packages/app-lib/src/config.rs | 3 + packages/app-lib/src/error.rs | 6 + packages/app-lib/src/event/emit.rs | 7 +- packages/app-lib/src/event/mod.rs | 14 +- packages/app-lib/src/launcher/download.rs | 12 +- packages/app-lib/src/launcher/mod.rs | 188 +- packages/app-lib/src/lib.rs | 1 - packages/app-lib/src/state/cache.rs | 855 +++++++++ packages/app-lib/src/state/children.rs | 36 +- packages/app-lib/src/state/db/mod.rs | 0 packages/app-lib/src/state/dirs.rs | 36 +- packages/app-lib/src/state/discord.rs | 18 +- packages/app-lib/src/state/java_globals.rs | 130 +- packages/app-lib/src/state/metadata.rs | 173 -- packages/app-lib/src/state/mod.rs | 286 +-- packages/app-lib/src/state/mr_auth.rs | 20 +- packages/app-lib/src/state/profiles.rs | 1645 +++++++---------- packages/app-lib/src/state/projects.rs | 807 -------- packages/app-lib/src/state/settings.rs | 299 +-- packages/app-lib/src/state/tags.rs | 261 --- packages/app-lib/src/util/fetch.rs | 42 +- packages/app-lib/src/util/jre.rs | 26 +- 47 files changed, 3301 insertions(+), 4609 deletions(-) create mode 100644 .env create mode 100644 packages/app-lib/src/state/cache.rs create mode 100644 packages/app-lib/src/state/db/mod.rs delete mode 100644 packages/app-lib/src/state/metadata.rs delete mode 100644 packages/app-lib/src/state/projects.rs delete mode 100644 packages/app-lib/src/state/tags.rs diff --git a/.env b/.env new file mode 100644 index 000000000..0e20bad10 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:app-playground-data/app.db \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6c40725a0..408aecadd 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ apps/frontend/src/generated .turbo target generated + +# app testing dir +app-playground-data/* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index be3ec252e..5a62b679f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,8 +130,8 @@ dependencies = [ "pin-project-lite", "tokio", "xz2", - "zstd 0.13.1", - "zstd-safe 7.1.0", + "zstd 0.13.2", + "zstd-safe 7.2.0", ] [[package]] @@ -243,7 +243,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -272,13 +272,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -607,9 +607,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.104" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "907d8581360765417f8f2e0e7d602733bbed60156b4465b7617243689ef9b83d" dependencies = [ "jobserver", "libc", @@ -630,7 +630,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid 1.9.1", + "uuid 1.10.0", ] [[package]] @@ -990,7 +990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -1000,7 +1000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -1022,9 +1022,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -1032,27 +1032,42 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.68", + "syn 2.0.70", +] + +[[package]] +name = "dashmap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", ] [[package]] @@ -1068,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ "serde", - "uuid 1.9.1", + "uuid 1.10.0", ] [[package]] @@ -1119,7 +1134,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -1317,7 +1332,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -1507,7 +1522,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -1645,7 +1660,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2240,9 +2255,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -2264,9 +2279,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -2290,7 +2305,7 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "rustls", "rustls-pki-types", @@ -2306,7 +2321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.30", "native-tls", "tokio", "tokio-native-tls", @@ -2320,7 +2335,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "native-tls", "tokio", @@ -2339,7 +2354,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.4.0", + "hyper 1.4.1", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -2703,9 +2718,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ "cc", "pkg-config", @@ -2934,7 +2949,7 @@ dependencies = [ "crash-handler", "minidumper", "thiserror", - "uuid 1.9.1", + "uuid 1.10.0", ] [[package]] @@ -3135,7 +3150,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3314,7 +3329,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3588,7 +3603,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3635,7 +3650,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4064,7 +4079,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -4107,7 +4122,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -4256,9 +4271,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "once_cell", "rustls-pki-types", @@ -4362,7 +4377,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4547,27 +4562,27 @@ dependencies = [ "thiserror", "time", "url", - "uuid 1.9.1", + "uuid 1.10.0", ] [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4601,7 +4616,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4627,9 +4642,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.2" +version = "3.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079f3a42cd87588d924ed95b533f8d30a483388c4e400ab736a7058e34f16169" +checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" dependencies = [ "base64 0.22.1", "chrono", @@ -4645,14 +4660,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.2" +version = "3.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc03aad67c1d26b7de277d51c86892e7d9a0110a2fe44bf6b26cc569fba302d6" +checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4778,6 +4793,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "smart-default" @@ -4787,7 +4805,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4869,9 +4887,8 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +version = "0.8.0-alpha.0" +source = "git+https://github.com/launchbadge/sqlx.git?rev=352b02de6af70f1ff1bfbd15329120589a0f7337#352b02de6af70f1ff1bfbd15329120589a0f7337" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4882,9 +4899,8 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +version = "0.8.0-alpha.0" +source = "git+https://github.com/launchbadge/sqlx.git?rev=352b02de6af70f1ff1bfbd15329120589a0f7337#352b02de6af70f1ff1bfbd15329120589a0f7337" dependencies = [ "ahash", "atoi", @@ -4921,22 +4937,20 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +version = "0.8.0-alpha.0" +source = "git+https://github.com/launchbadge/sqlx.git?rev=352b02de6af70f1ff1bfbd15329120589a0f7337#352b02de6af70f1ff1bfbd15329120589a0f7337" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.70", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +version = "0.8.0-alpha.0" +source = "git+https://github.com/launchbadge/sqlx.git?rev=352b02de6af70f1ff1bfbd15329120589a0f7337#352b02de6af70f1ff1bfbd15329120589a0f7337" dependencies = [ "dotenvy", "either", @@ -4951,7 +4965,7 @@ dependencies = [ "sqlx-core", "sqlx-mysql", "sqlx-sqlite", - "syn 1.0.109", + "syn 2.0.70", "tempfile", "tokio", "url", @@ -4959,9 +4973,8 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +version = "0.8.0-alpha.0" +source = "git+https://github.com/launchbadge/sqlx.git?rev=352b02de6af70f1ff1bfbd15329120589a0f7337#352b02de6af70f1ff1bfbd15329120589a0f7337" dependencies = [ "atoi", "base64 0.21.7", @@ -5001,9 +5014,8 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +version = "0.8.0-alpha.0" +source = "git+https://github.com/launchbadge/sqlx.git?rev=352b02de6af70f1ff1bfbd15329120589a0f7337#352b02de6af70f1ff1bfbd15329120589a0f7337" dependencies = [ "atoi", "base64 0.21.7", @@ -5039,9 +5051,8 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +version = "0.8.0-alpha.0" +source = "git+https://github.com/launchbadge/sqlx.git?rev=352b02de6af70f1ff1bfbd15329120589a0f7337#352b02de6af70f1ff1bfbd15329120589a0f7337" dependencies = [ "atoi", "flume", @@ -5143,9 +5154,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" dependencies = [ "proc-macro2", "quote", @@ -5189,9 +5200,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.12" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" dependencies = [ "cfg-if", "core-foundation-sys", @@ -5290,7 +5301,7 @@ dependencies = [ "serde", "tao-macros", "unicode-segmentation", - "uuid 1.9.1", + "uuid 1.10.0", "windows 0.39.0", "windows-implement", "x11-dl", @@ -5320,9 +5331,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" [[package]] name = "tauri" @@ -5370,7 +5381,7 @@ dependencies = [ "thiserror", "tokio", "url", - "uuid 1.9.1", + "uuid 1.10.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -5417,7 +5428,7 @@ dependencies = [ "tauri-utils", "thiserror", "time", - "uuid 1.9.1", + "uuid 1.10.0", "walkdir", ] @@ -5454,7 +5465,7 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" version = "0.0.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76b0f657d1ce0eb273f5b31b6ddf056c7a185d0b" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#a32008965b5cf060cc04e1e143dd221ce410715d" dependencies = [ "log", "serde", @@ -5468,7 +5479,7 @@ dependencies = [ [[package]] name = "tauri-plugin-window-state" version = "0.1.1" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76b0f657d1ce0eb273f5b31b6ddf056c7a185d0b" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#a32008965b5cf060cc04e1e143dd221ce410715d" dependencies = [ "bincode 1.3.3", "bitflags 2.6.0", @@ -5495,7 +5506,7 @@ dependencies = [ "tauri-utils", "thiserror", "url", - "uuid 1.9.1", + "uuid 1.10.0", "webview2-com", "windows 0.39.0", ] @@ -5513,7 +5524,7 @@ dependencies = [ "raw-window-handle", "tauri-runtime", "tauri-utils", - "uuid 1.9.1", + "uuid 1.10.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -5595,6 +5606,7 @@ dependencies = [ "bytes", "chrono", "daedalus", + "dashmap", "dirs", "discord-rich-presence", "dunce", @@ -5630,7 +5642,7 @@ dependencies = [ "tracing-subscriber", "url", "urlencoding", - "uuid 1.9.1", + "uuid 1.10.0", "whoami", "winreg 0.52.0", "zip", @@ -5665,7 +5677,7 @@ dependencies = [ "tracing", "tracing-error", "url", - "uuid 1.9.1", + "uuid 1.10.0", "window-shadows", ] @@ -5674,7 +5686,7 @@ name = "theseus_macros" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -5695,7 +5707,7 @@ dependencies = [ "tracing-error", "tracing-subscriber", "url", - "uuid 1.9.1", + "uuid 1.10.0", "webbrowser", ] @@ -5707,22 +5719,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -5768,9 +5780,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -5814,7 +5826,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -5893,7 +5905,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.15", ] [[package]] @@ -5920,9 +5932,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ "indexmap 2.2.6", "serde", @@ -5990,7 +6002,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -6157,9 +6169,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.7" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ "base64 0.22.1", "log", @@ -6203,9 +6215,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", "serde", @@ -6337,7 +6349,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "wasm-bindgen-shared", ] @@ -6371,7 +6383,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7147,7 +7159,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -7187,11 +7199,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe 7.1.0", + "zstd-safe 7.2.0", ] [[package]] @@ -7206,18 +7218,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.11+zstd.1.5.6" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 5713c74be..4475dc6cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,6 @@ codegen-units = 1 # Compile crates one after another so the compiler can optimiz lto = true # Enables link to optimizations opt-level = "s" # Optimize for binary size strip = true # Remove debug symbols + +[profile.dev.package.sqlx-macros] +opt-level = 3 diff --git a/apps/app-playground/src/main.rs b/apps/app-playground/src/main.rs index 185b972ec..b63082dfa 100644 --- a/apps/app-playground/src/main.rs +++ b/apps/app-playground/src/main.rs @@ -39,6 +39,7 @@ async fn main() -> theseus::Result<()> { let _log_guard = theseus::start_logger(); // Initialize state + State::init().await?; let st = State::get().await?; //State::update(); @@ -46,14 +47,6 @@ async fn main() -> theseus::Result<()> { println!("No users found, authenticating."); authenticate_run().await?; // could take credentials from here direct, but also deposited in state users } - - // Autodetect java globals - st.settings.write().await.max_concurrent_downloads = 50; - st.settings.write().await.hooks.post_exit = - Some("echo This is after Minecraft runs- global setting!".to_string()); - // Changed the settings, so need to reset the semaphore - st.reset_fetch_semaphore().await; - // // st.settings // .write() @@ -63,7 +56,7 @@ async fn main() -> theseus::Result<()> { // Clear profiles println!("Clearing profiles."); { - let h = profile::list(None).await?; + let h = profile::list().await?; for (path, _) in h.into_iter() { profile::remove(&path).await?; } @@ -73,7 +66,7 @@ async fn main() -> theseus::Result<()> { let name = "Example".to_string(); let game_version = "1.19.2".to_string(); - let modloader = ModLoader::Vanilla; + let modloader = ModLoader::Fabric; let loader_version = "stable".to_string(); let profile_path = profile_create( @@ -85,12 +78,9 @@ async fn main() -> theseus::Result<()> { None, None, None, - None, ) .await?; - State::sync().await?; - println!("running"); // Run a profile, running minecraft and store the RwLock to the process let proc_lock = profile::run(&profile_path).await?; diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml index af93f5a38..572b13ba7 100644 --- a/apps/app/Cargo.toml +++ b/apps/app/Cargo.toml @@ -60,7 +60,3 @@ default = ["custom-protocol"] # this feature is used for production builds where `devPath` points to the filesystem # DO NOT remove this custom-protocol = ["tauri/custom-protocol"] - -[profile.dev.package.sqlx-macros] -opt-level = 3 - diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index e14808e91..315eb64d8 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -23,7 +23,7 @@ async_zip = { version = "0.0.17", features = ["full"] } flate2 = "1.0.28" tempfile = "3.5.0" urlencoding = "2.1.3" - +dashmap = { version = "6.0.1", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] } daedalus = { version = "0.1.25" } @@ -66,7 +66,10 @@ rand = "0.8" byteorder = "1.5.0" base64 = "0.22.0" -sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "macros"] } +# TODO: Remove when new SQLX version is released +# We force-upgrade SQLite so JSONB support is added (theseus) +# https://github.com/launchbadge/sqlx/commit/352b02de6af70f1ff1bfbd15329120589a0f7337 +sqlx = { git = "https://github.com/launchbadge/sqlx.git", rev = "352b02de6af70f1ff1bfbd15329120589a0f7337", features = [ "runtime-tokio", "sqlite", "macros"] } [target.'cfg(windows)'.dependencies] winreg = "0.52.0" diff --git a/packages/app-lib/migrations/20240711194701_init.sql b/packages/app-lib/migrations/20240711194701_init.sql index 6815b667a..ce3b5e683 100644 --- a/packages/app-lib/migrations/20240711194701_init.sql +++ b/packages/app-lib/migrations/20240711194701_init.sql @@ -1,25 +1,31 @@ CREATE TABLE settings ( id INTEGER NOT NULL CHECK (id = 0), + max_concurrent_downloads INTEGER NOT NULL DEFAULT 10, + max_concurrent_writes INTEGER NOT NULL DEFAULT 10, + theme TEXT NOT NULL DEFAULT 'dark', default_page TEXT NOT NULL DEFAULT 'home', collapsed_navigation INTEGER NOT NULL DEFAULT TRUE, - discord_rpc INTEGER NOT NULL DEFAULT TRUE, - hide_on_process_start INTEGER NOT NULL DEFAULT FALSE, + advanced_rendering INTEGER NOT NULL DEFAULT TRUE, native_decorations INTEGER NOT NULL DEFAULT FALSE, - developer_mode INTEGER NOT NULL DEFAULT FALSE, + telemetry INTEGER NOT NULL DEFAULT TRUE, - advanced_rendering INTEGER NOT NULL DEFAULT TRUE, + discord_rpc INTEGER NOT NULL DEFAULT TRUE, + developer_mode INTEGER NOT NULL DEFAULT FALSE, + onboarded INTEGER NOT NULL DEFAULT FALSE, + -- array of strings + extra_launch_args JSONB NOT NULL, + -- array of (string, string) + custom_env_vars JSONB NOT NULL, mc_memory_max INTEGER NOT NULL DEFAULT 2048, mc_force_fullscreen INTEGER NOT NULL DEFAULT FALSE, mc_game_resolution_x INTEGER NOT NULL DEFAULT 854, mc_game_resolution_y INTEGER NOT NULL DEFAULT 480, - -- array of strings - custom_launch_args JSONB NOT NULL DEFAULT jsonb_array(), - -- array of (string, string) - custom_env_args JSONB NOT NULL DEFAULT jsonb_array(), + + hide_on_process_start INTEGER NOT NULL DEFAULT FALSE, hook_pre_launch TEXT NULL, hook_wrapper TEXT NULL, @@ -28,7 +34,7 @@ CREATE TABLE settings ( PRIMARY KEY (id) ); -INSERT INTO settings (id) VALUES (0); +INSERT INTO settings (id, extra_launch_args, custom_env_vars) VALUES (0, jsonb_array(), jsonb_array()); CREATE TABLE java_versions ( major_version INTEGER NOT NULL, @@ -51,6 +57,21 @@ CREATE TABLE minecraft_users ( ); CREATE UNIQUE INDEX minecraft_users_active ON minecraft_users(active); +CREATE TABLE minecraft_device_tokens ( + id INTEGER NOT NULL CHECK (id = 0), + + uuid BLOB NOT NULL, + private_key TEXT NOT NULL, + x TEXT NOT NULL, + y TEXT NOT NULL, + issue_instant INTEGER NOT NULL, + not_after INTEGER NOT NULL, + token TEXT NOT NULL, + display_claims JSONB NOT NULL, + + PRIMARY KEY (id) +); + CREATE TABLE modrinth_users ( id INTEGER NOT NULL, active INTEGER NOT NULL DEFAULT FALSE, @@ -59,10 +80,10 @@ CREATE TABLE modrinth_users ( PRIMARY KEY (id) ); -CREATE UNIQUE INDEX minecraft_users_active ON minecraft_users(active); +CREATE UNIQUE INDEX modrinth_users_active ON modrinth_users(active); -CREATE TABLE modrinth_cache ( - id INTEGER NOT NULL, +CREATE TABLE cache ( + id TEXT NOT NULL, data_type TEXT NOT NULL, alias TEXT NULL, @@ -73,7 +94,66 @@ CREATE TABLE modrinth_cache ( PRIMARY KEY (id, data_type) ); --- profiles --- safe processes --- file name hashes --- hashes project id version id \ No newline at end of file +CREATE TABLE path_hashes ( + path TEXT NOT NULL, + hash BLOB NOT NULL, + file_size INTEGER NOT NULL, + + PRIMARY KEY (path) +); + +CREATE TABLE profiles ( + path TEXT NOT NULL, + install_stage TEXT NOT NULL, + + name TEXT NOT NULL, + icon_path TEXT NULL, + + game_version TEXT NOT NULL, + mod_loader TEXT NOT NULL, + mod_loader_version TEXT NULL, + + linked_project_id TEXT NULL, + linked_version_id TEXT NULL, + locked INTEGER NULL, + + created INTEGER NOT NULL, + modified INTEGER NOT NULL, + last_played INTEGER NULL, + + submitted_time_played INTEGER NOT NULL DEFAULT 0, + recent_time_played INTEGER NOT NULL DEFAULT 0, + + override_java_path TEXT NULL, + + -- array of strings + override_extra_launch_args JSONB NOT NULL, + -- array of (string, string) + override_custom_env_vars JSONB NOT NULL, + + override_mc_memory_max INTEGER NULL, + override_mc_force_fullscreen INTEGER NULL, + override_mc_game_resolution_x INTEGER NULL, + override_mc_game_resolution_y INTEGER NULL, + + override_hook_pre_launch TEXT NULL, + override_hook_wrapper TEXT NULL, + override_hook_post_exit TEXT NULL, + + + PRIMARY KEY (path) +); + +CREATE TABLE processes ( + pid INTEGER NOT NULL, + uuid BLOB NOT NULL, + start_time INTEGER NOT NULL, + name TEXT NOT NULL, + executable TEXT NOT NULL, + profile_path TEXT NOT NULL, + post_exit_command TEXT NULL, + + UNIQUE (pid), + PRIMARY KEY (pid), + FOREIGN KEY (profile_path) REFERENCES profile(path) +); diff --git a/packages/app-lib/src/api/jre.rs b/packages/app-lib/src/api/jre.rs index e3e2014d0..046d36dd1 100644 --- a/packages/app-lib/src/api/jre.rs +++ b/packages/app-lib/src/api/jre.rs @@ -4,13 +4,13 @@ use serde::Deserialize; use std::path::PathBuf; use crate::event::emit::{emit_loading, init_loading}; -use crate::state::CredentialsStore; +use crate::state::JavaVersion; use crate::util::fetch::{fetch_advanced, fetch_json}; use crate::util::io; use crate::util::jre::extract_java_majorminor_version; use crate::{ - util::jre::{self, JavaVersion}, + util::jre::{self}, LoadingBarType, State, }; @@ -67,7 +67,6 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result { None, None, &state.fetch_semaphore, - &CredentialsStore(None), ).await?; emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?; @@ -80,7 +79,6 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result { None, Some((&loading_bar, 80.0)), &state.fetch_semaphore, - &CredentialsStore(None), ) .await?; diff --git a/packages/app-lib/src/api/logs.rs b/packages/app-lib/src/api/logs.rs index 9ae51ade7..54dd17226 100644 --- a/packages/app-lib/src/api/logs.rs +++ b/packages/app-lib/src/api/logs.rs @@ -9,9 +9,9 @@ use tokio::{ }; use crate::{ - prelude::{Credentials, DirectoryInfo}, + prelude::Credentials, util::io::{self, IOError}, - {state::ProfilePathId, State}, + State, }; #[derive(Serialize, Debug)] @@ -66,7 +66,7 @@ impl Logs { async fn build( log_type: LogType, age: SystemTime, - profile_subpath: &ProfilePathId, + profile_subpath: &str, filename: String, clear_contents: Option, ) -> crate::Result { @@ -95,19 +95,22 @@ impl Logs { #[tracing::instrument] pub async fn get_logs_from_type( - profile_path: &ProfilePathId, + profile_path: &str, log_type: LogType, clear_contents: Option, logs: &mut Vec>, ) -> crate::Result<()> { + let state = State::get().await?; + let logs_folder = match log_type { LogType::InfoLog => { - DirectoryInfo::profile_logs_dir(profile_path).await? + state.directories.profile_logs_dir(profile_path).await? } LogType::CrashReport => { - DirectoryInfo::crash_reports_dir(profile_path).await? + state.directories.crash_reports_dir(profile_path).await? } }; + if logs_folder.exists() { for entry in std::fs::read_dir(&logs_folder) .map_err(|e| IOError::with_path(e, &logs_folder))? @@ -142,21 +145,19 @@ pub async fn get_logs_from_type( #[tracing::instrument] pub async fn get_logs( - profile_path_id: ProfilePathId, + profile_path_id: &str, clear_contents: Option, ) -> crate::Result> { - let profile_path = profile_path_id.profile_path().await?; - let mut logs = Vec::new(); get_logs_from_type( - &profile_path, + profile_path_id, LogType::InfoLog, clear_contents, &mut logs, ) .await?; get_logs_from_type( - &profile_path, + profile_path_id, LogType::CrashReport, clear_contents, &mut logs, @@ -170,31 +171,31 @@ pub async fn get_logs( #[tracing::instrument] pub async fn get_logs_by_filename( - profile_path_id: ProfilePathId, + profile_path: &str, log_type: LogType, filename: String, ) -> crate::Result { - let profile_path = profile_path_id.profile_path().await?; + let state = State::get().await?; let path = match log_type { LogType::InfoLog => { - DirectoryInfo::profile_logs_dir(&profile_path).await + state.directories.profile_logs_dir(profile_path).await? } LogType::CrashReport => { - DirectoryInfo::crash_reports_dir(&profile_path).await + state.directories.crash_reports_dir(profile_path).await? } - }? + } .join(&filename); let metadata = std::fs::metadata(&path)?; let age = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH); - Logs::build(log_type, age, &profile_path, filename, Some(true)).await + Logs::build(log_type, age, profile_path, filename, Some(true)).await } #[tracing::instrument] pub async fn get_output_by_filename( - profile_subpath: &ProfilePathId, + profile_subpath: &str, log_type: LogType, file_name: &str, ) -> crate::Result { @@ -202,10 +203,10 @@ pub async fn get_output_by_filename( let logs_folder = match log_type { LogType::InfoLog => { - DirectoryInfo::profile_logs_dir(profile_subpath).await? + state.directories.profile_logs_dir(profile_subpath).await? } LogType::CrashReport => { - DirectoryInfo::crash_reports_dir(profile_subpath).await? + state.directories.crash_reports_dir(profile_subpath).await? } }; @@ -265,10 +266,11 @@ pub async fn get_output_by_filename( } #[tracing::instrument] -pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> { - let profile_path = profile_path_id.profile_path().await?; +pub async fn delete_logs(profile_path_id: &str) -> crate::Result<()> { + let state = State::get().await?; - let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?; + let logs_folder = + state.directories.profile_logs_dir(profile_path_id).await?; for entry in std::fs::read_dir(&logs_folder) .map_err(|e| IOError::with_path(e, &logs_folder))? { @@ -283,20 +285,20 @@ pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> { #[tracing::instrument] pub async fn delete_logs_by_filename( - profile_path_id: ProfilePathId, + profile_path_id: &str, log_type: LogType, filename: &str, ) -> crate::Result<()> { - let profile_path = profile_path_id.profile_path().await?; + let state = State::get().await?; let logs_folder = match log_type { LogType::InfoLog => { - DirectoryInfo::profile_logs_dir(&profile_path).await + state.directories.profile_logs_dir(profile_path_id).await? } LogType::CrashReport => { - DirectoryInfo::crash_reports_dir(&profile_path).await + state.directories.crash_reports_dir(profile_path_id).await? } - }?; + }; let path = logs_folder.join(filename); io::remove_dir_all(&path).await?; @@ -305,7 +307,7 @@ pub async fn delete_logs_by_filename( #[tracing::instrument] pub async fn get_latest_log_cursor( - profile_path: ProfilePathId, + profile_path: &str, cursor: u64, // 0 to start at beginning of file ) -> crate::Result { get_generic_live_log_cursor(profile_path, "latest.log", cursor).await @@ -313,14 +315,13 @@ pub async fn get_latest_log_cursor( #[tracing::instrument] pub async fn get_generic_live_log_cursor( - profile_path_id: ProfilePathId, + profile_path_id: &str, log_file_name: &str, mut cursor: u64, // 0 to start at beginning of file ) -> crate::Result { - let profile_path = profile_path_id.profile_path().await?; - let state = State::get().await?; - let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?; + let logs_folder = + state.directories.profile_logs_dir(profile_path_id).await?; let path = logs_folder.join(log_file_name); if !path.exists() { // Allow silent failure if latest.log doesn't exist (as the instance may have been launched, but not yet created the file) diff --git a/packages/app-lib/src/api/metadata.rs b/packages/app-lib/src/api/metadata.rs index 1a6b56a70..dc2d20a0e 100644 --- a/packages/app-lib/src/api/metadata.rs +++ b/packages/app-lib/src/api/metadata.rs @@ -1,3 +1,4 @@ +use crate::state::CachedEntry; use crate::State; pub use daedalus::minecraft::VersionManifest; pub use daedalus::modded::Manifest; @@ -5,39 +6,32 @@ pub use daedalus::modded::Manifest; #[tracing::instrument] pub async fn get_minecraft_versions() -> crate::Result { let state = State::get().await?; - let tags = state.metadata.read().await.minecraft.clone(); - - Ok(tags) + let minecraft_versions = CachedEntry::get_minecraft_manifest( + None, + &state.pool, + &state.fetch_semaphore, + ) + .await? + .ok_or_else(|| { + crate::ErrorKind::NoValueFor("minecraft versions".to_string()) + })?; + + Ok(minecraft_versions) } #[tracing::instrument] -pub async fn get_fabric_versions() -> crate::Result { +pub async fn get_loader_versions(loader: &str) -> crate::Result { let state = State::get().await?; - let tags = state.metadata.read().await.fabric.clone(); - - Ok(tags) -} - -#[tracing::instrument] -pub async fn get_forge_versions() -> crate::Result { - let state = State::get().await?; - let tags = state.metadata.read().await.forge.clone(); - - Ok(tags) -} - -#[tracing::instrument] -pub async fn get_quilt_versions() -> crate::Result { - let state = State::get().await?; - let tags = state.metadata.read().await.quilt.clone(); - - Ok(tags) -} - -#[tracing::instrument] -pub async fn get_neoforge_versions() -> crate::Result { - let state = State::get().await?; - let tags = state.metadata.read().await.neoforge.clone(); - - Ok(tags) + let loaders = CachedEntry::get_loader_manifest( + loader, + None, + &state.pool, + &state.fetch_semaphore, + ) + .await? + .ok_or_else(|| { + crate::ErrorKind::NoValueFor(format!("{} loader versions", loader)) + })?; + + Ok(loaders.manifest) } diff --git a/packages/app-lib/src/api/mod.rs b/packages/app-lib/src/api/mod.rs index 97b7991da..1d46f6b7e 100644 --- a/packages/app-lib/src/api/mod.rs +++ b/packages/app-lib/src/api/mod.rs @@ -14,11 +14,9 @@ pub mod tags; pub mod data { pub use crate::state::{ - Credentials, DirectoryInfo, Hooks, JavaSettings, LinkedData, - MemorySettings, ModLoader, ModrinthCredentials, - ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember, - ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata, - Settings, Theme, WindowSize, + Credentials, DirectoryInfo, Hooks, LinkedData, MemorySettings, + ModLoader, ModrinthCredentials, ModrinthCredentialsResult, Settings, + Theme, WindowSize, }; } @@ -29,12 +27,8 @@ pub mod prelude { jre, metadata, minecraft_auth, pack, process, profile::{self, create, Profile}, settings, - state::JavaGlobals, - state::{Dependency, ProfilePathId, ProjectPathId}, - util::{ - io::{canonicalize, IOError}, - jre::JavaVersion, - }, + state::Dependency, + util::io::{canonicalize, IOError}, State, }; } diff --git a/packages/app-lib/src/api/pack/import/atlauncher.rs b/packages/app-lib/src/api/pack/import/atlauncher.rs index 96be1fd4d..4f99858f0 100644 --- a/packages/app-lib/src/api/pack/import/atlauncher.rs +++ b/packages/app-lib/src/api/pack/import/atlauncher.rs @@ -8,7 +8,7 @@ use crate::{ import::{self, copy_dotminecraft}, install_from::CreatePackDescription, }, - prelude::{ModLoader, Profile, ProfilePathId}, + prelude::ModLoader, state::{LinkedData, ProfileInstallStage}, util::io, State, @@ -120,7 +120,7 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool { pub async fn import_atlauncher( atlauncher_base_path: PathBuf, // path to base atlauncher folder instance_folder: String, // instance folder in atlauncher_base_path - profile_path: ProfilePathId, // path to profile + profile_path: &str, // path to profile ) -> crate::Result<()> { let atlauncher_instance_path = atlauncher_base_path .join("instances") @@ -159,7 +159,7 @@ pub async fn import_atlauncher( project_id: None, version_id: None, existing_loading_bar: None, - profile_path: profile_path.clone(), + profile_path: profile_path.to_string(), }; let backup_name = format!("ATLauncher-{}", instance_folder); @@ -177,7 +177,7 @@ pub async fn import_atlauncher( } async fn import_atlauncher_unmanaged( - profile_path: ProfilePathId, + profile_path: &str, minecraft_folder: PathBuf, backup_name: String, description: CreatePackDescription, @@ -198,10 +198,10 @@ async fn import_atlauncher_unmanaged( let game_version = atinstance.id; let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile::create::get_loader_version_from_loader( - game_version.clone(), + crate::launcher::get_loader_version_from_profile( + &game_version, mod_loader, - Some(atinstance.launcher.loader_version.version.clone()), + Some(&atinstance.launcher.loader_version.version), ) .await? } else { @@ -209,24 +209,30 @@ async fn import_atlauncher_unmanaged( }; // Set profile data to created default profile - crate::api::profile::edit(&profile_path, |prof| { - prof.metadata.name = description + crate::api::profile::edit(profile_path, |prof| { + prof.name = description .override_title .clone() .unwrap_or_else(|| backup_name.to_string()); prof.install_stage = ProfileInstallStage::PackInstalling; - prof.metadata.linked_data = Some(LinkedData { - project_id: description.project_id.clone(), - version_id: description.version_id.clone(), - locked: Some( - description.project_id.is_some() - && description.version_id.is_some(), - ), - }); - prof.metadata.icon.clone_from(&description.icon); - prof.metadata.game_version.clone_from(&game_version); - prof.metadata.loader_version.clone_from(&loader_version); - prof.metadata.loader = mod_loader; + + if let Some(ref project_id) = description.project_id { + if let Some(ref version_id) = description.version_id { + prof.linked_data = Some(LinkedData { + project_id: project_id.clone(), + version_id: version_id.clone(), + locked: true, + }) + } + } + + prof.icon_path = description + .icon + .clone() + .map(|x| x.to_string_lossy().to_string()); + prof.game_version.clone_from(&game_version); + prof.loader_version = loader_version.clone().map(|x| x.id); + prof.loader = mod_loader; async { Ok(()) } }) @@ -235,32 +241,20 @@ async fn import_atlauncher_unmanaged( // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) let state = State::get().await?; let loading_bar = copy_dotminecraft( - profile_path.clone(), + profile_path, minecraft_folder, &state.io_semaphore, None, ) .await?; - if let Some(profile_val) = - crate::api::profile::get(&profile_path, None).await? - { + if let Some(profile_val) = crate::api::profile::get(profile_path).await? { crate::launcher::install_minecraft( &profile_val, Some(loading_bar), false, ) .await?; - { - let state = State::get().await?; - let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs( - &profile_val.get_profile_full_path().await?, - &mut file_watcher, - ) - .await?; - } - State::sync().await?; } Ok(()) } diff --git a/packages/app-lib/src/api/pack/import/curseforge.rs b/packages/app-lib/src/api/pack/import/curseforge.rs index e8e056d83..4fcb720f2 100644 --- a/packages/app-lib/src/api/pack/import/curseforge.rs +++ b/packages/app-lib/src/api/pack/import/curseforge.rs @@ -2,10 +2,8 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use crate::prelude::Profile; -use crate::state::CredentialsStore; use crate::{ - prelude::{ModLoader, ProfilePathId}, + prelude::ModLoader, state::ProfileInstallStage, util::{ fetch::{fetch, write_cached_icon}, @@ -49,7 +47,7 @@ pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool { pub async fn import_curseforge( curseforge_instance_folder: PathBuf, // instance's folder - profile_path: ProfilePathId, // path to profile + profile_path: &str, // path to profile ) -> crate::Result<()> { // Load minecraftinstance.json let minecraft_instance: String = io::read_to_string( @@ -77,13 +75,8 @@ pub async fn import_curseforge( thumbnail_url: Some(thumbnail_url), }) = minecraft_instance.installed_modpack.clone() { - let icon_bytes = fetch( - &thumbnail_url, - None, - &state.fetch_semaphore, - &CredentialsStore(None), - ) - .await?; + let icon_bytes = + fetch(&thumbnail_url, None, &state.fetch_semaphore).await?; let filename = thumbnail_url.rsplit('/').last(); if let Some(filename) = filename { icon = Some( @@ -121,10 +114,10 @@ pub async fn import_curseforge( let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla); let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile::create::get_loader_version_from_loader( - game_version.clone(), + crate::launcher::get_loader_version_from_profile( + &game_version, mod_loader, - loader_version, + loader_version.as_deref(), ) .await? } else { @@ -132,31 +125,32 @@ pub async fn import_curseforge( }; // Set profile data to created default profile - crate::api::profile::edit(&profile_path, |prof| { - prof.metadata.name = override_title + crate::api::profile::edit(profile_path, |prof| { + prof.name = override_title .clone() .unwrap_or_else(|| backup_name.to_string()); prof.install_stage = ProfileInstallStage::PackInstalling; - prof.metadata.icon.clone_from(&icon); - prof.metadata.game_version.clone_from(&game_version); - prof.metadata.loader_version.clone_from(&loader_version); - prof.metadata.loader = mod_loader; + prof.icon_path = + icon.clone().map(|x| x.to_string_lossy().to_string()); + prof.game_version.clone_from(&game_version); + prof.loader_version = loader_version.clone().map(|x| x.id); + prof.loader = mod_loader; async { Ok(()) } }) .await?; } else { // create a vanilla profile - crate::api::profile::edit(&profile_path, |prof| { - prof.metadata.name = override_title + crate::api::profile::edit(profile_path, |prof| { + prof.name = override_title .clone() .unwrap_or_else(|| backup_name.to_string()); - prof.metadata.icon.clone_from(&icon); - prof.metadata - .game_version + prof.icon_path = + icon.clone().map(|x| x.to_string_lossy().to_string()); + prof.game_version .clone_from(&minecraft_instance.game_version); - prof.metadata.loader_version = None; - prof.metadata.loader = ModLoader::Vanilla; + prof.loader_version = None; + prof.loader = ModLoader::Vanilla; async { Ok(()) } }) @@ -166,33 +160,20 @@ pub async fn import_curseforge( // Copy in contained folders as overrides let state = State::get().await?; let loading_bar = copy_dotminecraft( - profile_path.clone(), + profile_path, curseforge_instance_folder, &state.io_semaphore, None, ) .await?; - if let Some(profile_val) = - crate::api::profile::get(&profile_path, None).await? - { + if let Some(profile_val) = crate::api::profile::get(profile_path).await? { crate::launcher::install_minecraft( &profile_val, Some(loading_bar), false, ) .await?; - - { - let state = State::get().await?; - let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs( - &profile_val.get_profile_full_path().await?, - &mut file_watcher, - ) - .await?; - } - State::sync().await?; } Ok(()) diff --git a/packages/app-lib/src/api/pack/import/gdlauncher.rs b/packages/app-lib/src/api/pack/import/gdlauncher.rs index 5e5367edb..b6481e596 100644 --- a/packages/app-lib/src/api/pack/import/gdlauncher.rs +++ b/packages/app-lib/src/api/pack/import/gdlauncher.rs @@ -2,12 +2,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use crate::{ - prelude::{ModLoader, Profile, ProfilePathId}, - state::ProfileInstallStage, - util::io, - State, -}; +use crate::{prelude::ModLoader, state::ProfileInstallStage, util::io, State}; use super::{copy_dotminecraft, recache_icon}; @@ -41,7 +36,7 @@ pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool { pub async fn import_gdlauncher( gdlauncher_instance_folder: PathBuf, // instance's folder - profile_path: ProfilePathId, // path to profile + profile_path: &str, // path to profile ) -> crate::Result<()> { // Load config.json let config: String = @@ -74,10 +69,10 @@ pub async fn import_gdlauncher( let loader_version = config.loader.loader_version; let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile::create::get_loader_version_from_loader( - game_version.clone(), + crate::launcher::get_loader_version_from_profile( + &game_version, mod_loader, - loader_version, + loader_version.as_deref(), ) .await? } else { @@ -85,15 +80,15 @@ pub async fn import_gdlauncher( }; // Set profile data to created default profile - crate::api::profile::edit(&profile_path, |prof| { - prof.metadata.name = override_title + crate::api::profile::edit(profile_path, |prof| { + prof.name = override_title .clone() .unwrap_or_else(|| backup_name.to_string()); prof.install_stage = ProfileInstallStage::PackInstalling; - prof.metadata.icon.clone_from(&icon); - prof.metadata.game_version.clone_from(&game_version); - prof.metadata.loader_version.clone_from(&loader_version); - prof.metadata.loader = mod_loader; + prof.icon_path = icon.clone().map(|x| x.to_string_lossy().to_string()); + prof.game_version.clone_from(&game_version); + prof.loader_version = loader_version.clone().map(|x| x.id); + prof.loader = mod_loader; async { Ok(()) } }) @@ -102,32 +97,20 @@ pub async fn import_gdlauncher( // Copy in contained folders as overrides let state = State::get().await?; let loading_bar = copy_dotminecraft( - profile_path.clone(), + profile_path, gdlauncher_instance_folder, &state.io_semaphore, None, ) .await?; - if let Some(profile_val) = - crate::api::profile::get(&profile_path, None).await? - { + if let Some(profile_val) = crate::api::profile::get(profile_path).await? { crate::launcher::install_minecraft( &profile_val, Some(loading_bar), false, ) .await?; - { - let state = State::get().await?; - let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs( - &profile_val.get_profile_full_path().await?, - &mut file_watcher, - ) - .await?; - } - State::sync().await?; } Ok(()) diff --git a/packages/app-lib/src/api/pack/import/mmc.rs b/packages/app-lib/src/api/pack/import/mmc.rs index 9c2757f71..ae67d2412 100644 --- a/packages/app-lib/src/api/pack/import/mmc.rs +++ b/packages/app-lib/src/api/pack/import/mmc.rs @@ -7,7 +7,6 @@ use crate::{ import::{self, copy_dotminecraft}, install_from::{self, CreatePackDescription, PackDependency}, }, - prelude::{Profile, ProfilePathId}, util::io, State, }; @@ -178,9 +177,9 @@ async fn load_instance_cfg(file_path: &Path) -> crate::Result { #[tracing::instrument] #[theseus_macros::debug_pin] pub async fn import_mmc( - mmc_base_path: PathBuf, // path to base mmc folder - instance_folder: String, // instance folder in mmc_base_path - profile_path: ProfilePathId, // path to profile + mmc_base_path: PathBuf, // path to base mmc folder + instance_folder: String, // instance folder in mmc_base_path + profile_path: &str, // path to profile ) -> crate::Result<()> { let mmc_instance_path = mmc_base_path .join("instances") @@ -208,7 +207,7 @@ pub async fn import_mmc( project_id: instance_cfg.managed_pack_id, version_id: instance_cfg.managed_pack_version_id, existing_loading_bar: None, - profile_path: profile_path.clone(), + profile_path: profile_path.to_string(), }; // Managed pack @@ -260,7 +259,7 @@ pub async fn import_mmc( } async fn import_mmc_unmanaged( - profile_path: ProfilePathId, + profile_path: &str, minecraft_folder: PathBuf, backup_name: String, description: CreatePackDescription, @@ -302,7 +301,7 @@ async fn import_mmc_unmanaged( // Sets profile information to be that loaded from mmc-pack.json and instance.cfg install_from::set_profile_information( - profile_path.clone(), + profile_path.to_string(), &description, &backup_name, &dependencies, @@ -313,32 +312,20 @@ async fn import_mmc_unmanaged( // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) let state = State::get().await?; let loading_bar = copy_dotminecraft( - profile_path.clone(), + profile_path, minecraft_folder, &state.io_semaphore, None, ) .await?; - if let Some(profile_val) = - crate::api::profile::get(&profile_path, None).await? - { + if let Some(profile_val) = crate::api::profile::get(profile_path).await? { crate::launcher::install_minecraft( &profile_val, Some(loading_bar), false, ) .await?; - { - let state = State::get().await?; - let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs( - &profile_val.get_profile_full_path().await?, - &mut file_watcher, - ) - .await?; - } - State::sync().await?; } Ok(()) } diff --git a/packages/app-lib/src/api/pack/import/mod.rs b/packages/app-lib/src/api/pack/import/mod.rs index f4a384c9f..54e56a065 100644 --- a/packages/app-lib/src/api/pack/import/mod.rs +++ b/packages/app-lib/src/api/pack/import/mod.rs @@ -11,8 +11,6 @@ use crate::{ emit::{emit_loading, init_or_edit_loading}, LoadingBarId, }, - prelude::ProfilePathId, - state::Profiles, util::{ fetch::{self, IoSemaphore}, io, @@ -108,7 +106,7 @@ pub async fn get_importable_instances( #[theseus_macros::debug_pin] #[tracing::instrument] pub async fn import_instance( - profile_path: ProfilePathId, // This should be a blank profile + profile_path: &str, // This should be a blank profile launcher_type: ImportLauncherType, base_path: PathBuf, instance_folder: String, @@ -119,7 +117,7 @@ pub async fn import_instance( mmc::import_mmc( base_path, // path to base mmc folder instance_folder, // instance folder in mmc_base_path - profile_path.clone(), // path to profile + profile_path, // path to profile ) .await } @@ -127,21 +125,21 @@ pub async fn import_instance( atlauncher::import_atlauncher( base_path, // path to atlauncher folder instance_folder, // instance folder in atlauncher - profile_path.clone(), // path to profile + profile_path, // path to profile ) .await } ImportLauncherType::GDLauncher => { gdlauncher::import_gdlauncher( base_path.join("instances").join(instance_folder), // path to gdlauncher folder - profile_path.clone(), // path to profile + profile_path, // path to profile ) .await } ImportLauncherType::Curseforge => { curseforge::import_curseforge( base_path.join("Instances").join(instance_folder), // path to curseforge folder - profile_path.clone(), // path to profile + profile_path, // path to profile ) .await } @@ -158,14 +156,11 @@ pub async fn import_instance( Ok(_) => {} Err(e) => { tracing::warn!("Import failed: {:?}", e); - let _ = crate::api::profile::remove(&profile_path).await; + let _ = crate::api::profile::remove(profile_path).await; return Err(e); } } - // Check existing managed packs for potential updates - tokio::task::spawn(Profiles::update_modrinth_versions()); - tracing::debug!("Completed import."); Ok(()) } @@ -252,13 +247,14 @@ pub async fn recache_icon( } pub async fn copy_dotminecraft( - profile_path_id: ProfilePathId, + profile_path_id: &str, dotminecraft: PathBuf, io_semaphore: &IoSemaphore, existing_loading_bar: Option, ) -> crate::Result { // Get full path to profile - let profile_path = profile_path_id.get_full_path().await?; + let profile_path = + crate::api::profile::get_full_path(profile_path_id).await?; // Gets all subfiles recursively in src let subfiles = get_all_subfiles(&dotminecraft).await?; diff --git a/packages/app-lib/src/api/pack/install_from.rs b/packages/app-lib/src/api/pack/install_from.rs index 35f79a59b..93076c679 100644 --- a/packages/app-lib/src/api/pack/install_from.rs +++ b/packages/app-lib/src/api/pack/install_from.rs @@ -2,15 +2,15 @@ use crate::config::MODRINTH_API_URL; use crate::data::ModLoader; use crate::event::emit::{emit_loading, init_loading}; use crate::event::{LoadingBarId, LoadingBarType}; -use crate::prelude::ProfilePathId; use crate::state::{ - LinkedData, ModrinthProject, ModrinthVersion, ProfileInstallStage, SideType, + LinkedData, ProfileInstallStage, Project as ModrinthProject, SideType, + Version as ModrinthVersion, }; use crate::util::fetch::{ fetch, fetch_advanced, fetch_json, write_cached_icon, }; use crate::util::io; -use crate::{InnerProjectPathUnix, State}; +use crate::State; use reqwest::Method; use serde::{Deserialize, Serialize}; @@ -33,7 +33,7 @@ pub struct PackFormat { #[derive(Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PackFile { - pub path: InnerProjectPathUnix, + pub path: String, pub hashes: HashMap, pub env: Option>, pub downloads: Vec, @@ -144,7 +144,7 @@ pub struct CreatePackDescription { pub project_id: Option, pub version_id: Option, pub existing_loading_bar: Option, - pub profile_path: ProfilePathId, + pub profile_path: String, } pub fn get_profile_from_pack( @@ -160,9 +160,9 @@ pub fn get_profile_from_pack( name: title, icon_url, linked_data: Some(LinkedData { - project_id: Some(project_id), - version_id: Some(version_id), - locked: Some(true), + project_id, + version_id, + locked: true, }), ..Default::default() }, @@ -188,7 +188,7 @@ pub async fn generate_pack_from_version_id( version_id: String, title: String, icon_url: Option, - profile_path: ProfilePathId, + profile_path: String, // Existing loading bar. Unlike when existing_loading_bar is used, this one is pre-initialized with PackFileDownload // For example, you might use this if multiple packs are being downloaded at once and you want to use the same loading bar @@ -202,7 +202,7 @@ pub async fn generate_pack_from_version_id( } else { init_loading( LoadingBarType::PackFileDownload { - profile_path: profile_path.get_full_path().await?, + profile_path: profile_path.clone(), pack_name: title, icon: icon_url, pack_version: version_id.clone(), @@ -221,7 +221,6 @@ pub async fn generate_pack_from_version_id( None, None, &state.fetch_semaphore, - &creds, ) .await?; emit_loading(&loading_bar, 10.0, None).await?; @@ -249,7 +248,6 @@ pub async fn generate_pack_from_version_id( None, Some((&loading_bar, 70.0)), &state.fetch_semaphore, - &creds, ) .await?; emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?; @@ -260,15 +258,13 @@ pub async fn generate_pack_from_version_id( None, None, &state.fetch_semaphore, - &creds, ) .await?; emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?; let icon = if let Some(icon_url) = project.icon_url { let state = State::get().await?; - let icon_bytes = - fetch(&icon_url, None, &state.fetch_semaphore, &creds).await?; + let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?; drop(creds); let filename = icon_url.rsplit('/').next(); @@ -308,7 +304,7 @@ pub async fn generate_pack_from_version_id( #[theseus_macros::debug_pin] pub async fn generate_pack_from_file( path: PathBuf, - profile_path: ProfilePathId, + profile_path: String, ) -> crate::Result { let file = io::read(&path).await?; Ok(CreatePack { @@ -328,7 +324,7 @@ pub async fn generate_pack_from_file( /// This includes the pack name, icon, game version, loader version, and loader #[theseus_macros::debug_pin] pub async fn set_profile_information( - profile_path: ProfilePathId, + profile_path: String, description: &CreatePackDescription, backup_name: &str, dependencies: &HashMap, @@ -371,10 +367,10 @@ pub async fn set_profile_information( let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla); let loader_version = if mod_loader != ModLoader::Vanilla { - crate::profile::create::get_loader_version_from_loader( - game_version.clone(), + crate::launcher::get_loader_version_from_profile( + &game_version, mod_loader, - loader_version.cloned(), + loader_version.cloned().as_deref(), ) .await? } else { @@ -382,35 +378,36 @@ pub async fn set_profile_information( }; // Sets values in profile crate::api::profile::edit(&profile_path, |prof| { - prof.metadata.name = description + prof.name = description .override_title .clone() .unwrap_or_else(|| backup_name.to_string()); prof.install_stage = ProfileInstallStage::PackInstalling; - let project_id = description.project_id.clone(); - let version_id = description.version_id.clone(); - - prof.metadata.linked_data = if project_id.is_some() - && version_id.is_some() - { - Some(LinkedData { - project_id, - version_id, - locked: if !ignore_lock { - Some(true) - } else { - prof.metadata.linked_data.as_ref().and_then(|x| x.locked) - }, - }) - } else { - None - }; + if let Some(ref project_id) = description.project_id { + if let Some(ref version_id) = description.version_id { + prof.linked_data = Some(LinkedData { + project_id: project_id.clone(), + version_id: version_id.clone(), + locked: if !ignore_lock { + true + } else { + prof.linked_data + .as_ref() + .map(|x| x.locked) + .unwrap_or(true) + }, + }) + } + } - prof.metadata.icon.clone_from(&description.icon); - prof.metadata.game_version.clone_from(game_version); - prof.metadata.loader_version.clone_from(&loader_version); - prof.metadata.loader = mod_loader; + prof.icon_path = description + .icon + .clone() + .map(|x| x.to_string_lossy().to_string()); + prof.game_version.clone_from(game_version); + prof.loader_version = loader_version.clone().map(|x| x.id); + prof.loader = mod_loader; async { Ok(()) } }) diff --git a/packages/app-lib/src/api/pack/install_mrpack.rs b/packages/app-lib/src/api/pack/install_mrpack.rs index 42566bf45..cb75a6935 100644 --- a/packages/app-lib/src/api/pack/install_mrpack.rs +++ b/packages/app-lib/src/api/pack/install_mrpack.rs @@ -6,8 +6,7 @@ use crate::event::LoadingBarType; use crate::pack::install_from::{ set_profile_information, EnvType, PackFile, PackFileHash, }; -use crate::prelude::{ModrinthVersion, ProfilePathId, ProjectMetadata}; -use crate::state::{ProfileInstallStage, Profiles, SideType}; +use crate::state::{ProfileInstallStage, SideType, Version as ModrinthVersion}; use crate::util::fetch::{fetch_json, fetch_mirrors, write}; use crate::util::io; use crate::{profile, State}; @@ -31,8 +30,8 @@ use super::install_from::{ #[theseus_macros::debug_pin] pub async fn install_zipped_mrpack( location: CreatePackLocation, - profile_path: ProfilePathId, -) -> crate::Result { + profile_path: String, +) -> crate::Result { // Get file from description let create_pack: CreatePack = match location { CreatePackLocation::FromVersionId { @@ -59,9 +58,6 @@ pub async fn install_zipped_mrpack( // Install pack files, and if it fails, fail safely by removing the profile let result = install_zipped_mrpack_files(create_pack, false).await; - // Check existing managed packs for potential updates - tokio::task::spawn(Profiles::update_modrinth_versions()); - match result { Ok(profile) => Ok(profile), Err(err) => { @@ -78,7 +74,7 @@ pub async fn install_zipped_mrpack( pub async fn install_zipped_mrpack_files( create_pack: CreatePack, ignore_lock: bool, -) -> crate::Result { +) -> crate::Result { let state = &State::get().await?; let file = create_pack.file; @@ -132,7 +128,7 @@ pub async fn install_zipped_mrpack_files( let loading_bar = init_or_edit_loading( existing_loading_bar, LoadingBarType::PackDownload { - profile_path: profile_path.get_full_path().await?.clone(), + profile_path: profile_path.clone(), pack_name: pack.name.clone(), icon, pack_id: project_id, @@ -188,10 +184,10 @@ pub async fn install_zipped_mrpack_files( if let Some(path) = path { match path { Component::CurDir | Component::Normal(_) => { - let path = profile_path - .get_full_path() - .await? - .join(&project_path); + let path = + profile::get_full_path(&profile_path) + .await? + .join(&project_path); write(&path, &file, &state.io_semaphore) .await?; } @@ -244,7 +240,9 @@ pub async fn install_zipped_mrpack_files( if new_path.file_name().is_some() { write( - &profile_path.get_full_path().await?.join(new_path), + &profile::get_full_path(&profile_path) + .await? + .join(new_path), &content, &state.io_semaphore, ) @@ -265,24 +263,23 @@ pub async fn install_zipped_mrpack_files( // If the icon doesn't exist, we expect icon.png to be a potential icon. // If it doesn't exist, and an override to icon.png exists, cache and use that - let potential_icon = - profile_path.get_full_path().await?.join("icon.png"); + let potential_icon = profile::get_full_path(&profile_path) + .await? + .join("icon.png"); if !icon_exists && potential_icon.exists() { profile::edit_icon(&profile_path, Some(&potential_icon)).await?; } - if let Some(profile_val) = profile::get(&profile_path, None).await? { + if let Some(profile_val) = profile::get(&profile_path).await? { crate::launcher::install_minecraft( &profile_val, Some(loading_bar), false, ) .await?; - - State::sync().await?; } - Ok::(profile_path.clone()) + Ok::(profile_path.clone()) } else { Err(crate::Error::from(crate::ErrorKind::InputError( "No pack manifest found in mrpack".to_string(), @@ -293,7 +290,7 @@ pub async fn install_zipped_mrpack_files( #[tracing::instrument(skip(mrpack_file))] #[theseus_macros::debug_pin] pub async fn remove_all_related_files( - profile_path: ProfilePathId, + profile_path: String, mrpack_file: bytes::Bytes, ) -> crate::Result<()> { let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file); @@ -355,39 +352,20 @@ pub async fn remove_all_related_files( "algorithm": "sha512", })), &state.fetch_semaphore, - &creds, ) .await?; let to_remove = hash_projects .into_values() .map(|p| p.project_id) .collect::>(); - let profile = - profile::get(&profile_path, None).await?.ok_or_else(|| { - crate::ErrorKind::UnmanagedProfileError( - profile_path.to_string(), - ) - })?; - for (project_id, project) in &profile.projects { - if let ProjectMetadata::Modrinth { project, .. } = &project.metadata - { - if to_remove.contains(&project.id) { - let path = profile - .get_profile_full_path() - .await? - .join(project_id.0.clone()); - if path.exists() { - io::remove_file(&path).await?; - } - } - } - } + let profile = profile::get(&profile_path).await?.ok_or_else(|| { + crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) + })?; // Iterate over all Modrinth project file paths in the json, and remove them // (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized) for file in pack.files { - let path: PathBuf = profile_path - .get_full_path() + let path: PathBuf = profile::get_full_path(&profile_path) .await? .join(file.path.to_string()); if path.exists() { @@ -414,8 +392,9 @@ pub async fn remove_all_related_files( } // Remove this file if a corresponding one exists in the filesystem - let existing_file = - profile_path.get_full_path().await?.join(&new_path); + let existing_file = profile::get_full_path(&profile_path) + .await? + .join(&new_path); if existing_file.exists() { io::remove_file(&existing_file).await?; } diff --git a/packages/app-lib/src/api/process.rs b/packages/app-lib/src/api/process.rs index c75e940a3..f1720c304 100644 --- a/packages/app-lib/src/api/process.rs +++ b/packages/app-lib/src/api/process.rs @@ -2,11 +2,9 @@ use uuid::Uuid; -use crate::state::{MinecraftChild, ProfilePathId}; +use crate::state::MinecraftChild; pub use crate::{ - state::{ - Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, - }, + state::{Hooks, MemorySettings, Profile, Settings, WindowSize}, State, }; @@ -42,8 +40,7 @@ pub async fn get_all_running_uuids() -> crate::Result> { // Gets the Profile paths of each *running* stored process in the state #[tracing::instrument] -pub async fn get_all_running_profile_paths() -> crate::Result> -{ +pub async fn get_all_running_profile_paths() -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; children.running_profile_paths().await @@ -60,7 +57,7 @@ pub async fn get_all_running_profiles() -> crate::Result> { // Gets the UUID of each stored process in the state by profile path #[tracing::instrument] pub async fn get_uuids_by_profile_path( - profile_path: ProfilePathId, + profile_path: &str, ) -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; diff --git a/packages/app-lib/src/api/profile/create.rs b/packages/app-lib/src/api/profile/create.rs index 770da0523..b1d550315 100644 --- a/packages/app-lib/src/api/profile/create.rs +++ b/packages/app-lib/src/api/profile/create.rs @@ -1,22 +1,17 @@ //! Theseus profile management interface -use crate::pack::install_from::CreatePackProfile; -use crate::prelude::ProfilePathId; -use crate::state::LinkedData; +use crate::launcher::get_loader_version_from_profile; +use crate::settings::Hooks; +use crate::state::{LinkedData, ProfileInstallStage}; use crate::util::io::{self, canonicalize}; use crate::{ event::{emit::emit_profile, ProfilePayloadType}, prelude::ModLoader, }; use crate::{pack, profile, ErrorKind}; -pub use crate::{ - state::{JavaSettings, Profile}, - State, -}; -use daedalus::modded::LoaderVersion; +pub use crate::{state::Profile, State}; +use chrono::Utc; use std::path::PathBuf; - use tracing::{info, trace}; -use uuid::Uuid; // Creates a profile of a given name and adds it to the in-memory state // Returns relative filepath as ProfilePathId which can be used to access it in the State @@ -24,32 +19,30 @@ use uuid::Uuid; #[theseus_macros::debug_pin] #[allow(clippy::too_many_arguments)] pub async fn profile_create( - mut name: String, // the name of the profile, and relative path + name: String, // the name of the profile, and relative path game_version: String, // the game version of the profile modloader: ModLoader, // the modloader to use loader_version: Option, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest - icon: Option, // the icon for the profile - icon_url: Option, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES) + icon_path: Option, // the icon for the profile linked_data: Option, // the linked project ID (mainly for modpacks)- used for updating skip_install_profile: Option, no_watch: Option, -) -> crate::Result { - name = profile::sanitize_profile_name(&name); - +) -> crate::Result { trace!("Creating new profile. {}", name); let state = State::get().await?; - let uuid = Uuid::new_v4(); - let mut path = state.directories.profiles_dir().await.join(&name); - - if path.exists() { - let mut new_name; + let mut path = profile::sanitize_profile_name(&name); + let mut full_path = state.directories.profiles_dir().await.join(&path); + if full_path.exists() { let mut new_path; + let mut new_full_path; let mut which = 1; + loop { - new_name = format!("{name} ({which})"); - new_path = state.directories.profiles_dir().await.join(&new_name); - if !new_path.exists() { + new_path = format!("{path} ({which})"); + new_full_path = + state.directories.profiles_dir().await.join(&new_path); + if !new_full_path.exists() { break; } which += 1; @@ -57,32 +50,59 @@ pub async fn profile_create( tracing::debug!( "Folder collision: {}, renaming to: {}", - path.display(), - new_path.display() + full_path.display(), + new_full_path.display() ); + path = new_path; - name = new_name; + full_path = new_full_path; } - io::create_dir_all(&path).await?; + io::create_dir_all(&full_path).await?; info!( "Creating profile at path {}", - &canonicalize(&path)?.display() + &canonicalize(&full_path)?.display() ); let loader = if modloader != ModLoader::Vanilla { - get_loader_version_from_loader( - game_version.clone(), + get_loader_version_from_profile( + &game_version, modloader, - loader_version, + loader_version.as_deref(), ) .await? } else { None }; - let mut profile = Profile::new(uuid, name, game_version).await?; + let mut profile = Profile { + path: path.clone(), + install_stage: ProfileInstallStage::NotInstalled, + name, + icon_path: None, + game_version, + loader: modloader, + loader_version: loader.map(|x| x.id), + linked_data, + created: Utc::now(), + modified: Utc::now(), + last_played: None, + submitted_time_played: 0, + recent_time_played: 0, + java_path: None, + extra_launch_args: None, + custom_env_vars: None, + memory: None, + force_fullscreen: None, + game_resolution: None, + hooks: Hooks { + pre_launch: None, + wrapper: None, + post_exit: None, + }, + }; + let result = async { - if let Some(ref icon) = icon { + if let Some(ref icon) = icon_path { let bytes = io::read(state.directories.caches_dir().join(icon)).await?; profile @@ -90,92 +110,49 @@ pub async fn profile_create( &state.directories.caches_dir(), &state.io_semaphore, bytes::Bytes::from(bytes), - &icon.to_string_lossy(), + &icon, ) .await?; } - profile.metadata.icon_url = icon_url; - if let Some(loader_version) = loader { - profile.metadata.loader = modloader; - profile.metadata.loader_version = Some(loader_version); - } + emit_profile(&profile.path, &profile.name, ProfilePayloadType::Created) + .await?; - profile.metadata.linked_data = linked_data; - if let Some(linked_data) = &mut profile.metadata.linked_data { - linked_data.locked = Some( - linked_data.project_id.is_some() - && linked_data.version_id.is_some(), - ); - } - - emit_profile( - uuid, - &profile.profile_id(), - &profile.metadata.name, - ProfilePayloadType::Created, - ) - .await?; - - { - let mut profiles = state.profiles.write().await; - profiles - .insert(profile.clone(), no_watch.unwrap_or_default()) - .await?; - } + profile.upsert(&state.pool).await?; if !skip_install_profile.unwrap_or(false) { crate::launcher::install_minecraft(&profile, None, false).await?; } - State::sync().await?; - Ok(profile.profile_id()) + Ok(profile.path) } .await; match result { Ok(profile) => Ok(profile), Err(err) => { - let _ = crate::api::profile::remove(&profile.profile_id()).await; + let _ = profile::remove(&path).await; Err(err) } } } -pub async fn profile_create_from_creator( - profile: CreatePackProfile, -) -> crate::Result { - profile_create( - profile.name, - profile.game_version, - profile.modloader, - profile.loader_version, - profile.icon, - profile.icon_url, - profile.linked_data, - profile.skip_install_profile, - profile.no_watch, - ) - .await -} - pub async fn profile_create_from_duplicate( - copy_from: ProfilePathId, -) -> crate::Result { + copy_from: &str, +) -> crate::Result { // Original profile - let profile = profile::get(©_from, None).await?.ok_or_else(|| { + let profile = profile::get(©_from).await?.ok_or_else(|| { ErrorKind::UnmanagedProfileError(copy_from.to_string()) })?; let profile_path_id = profile_create( - profile.metadata.name.clone(), - profile.metadata.game_version.clone(), - profile.metadata.loader, - profile.metadata.loader_version.clone().map(|it| it.id), - profile.metadata.icon.clone(), - profile.metadata.icon_url.clone(), - profile.metadata.linked_data.clone(), + profile.name.clone(), + profile.game_version.clone(), + profile.loader, + profile.loader_version.clone(), + profile.icon_path.clone(), + profile.linked_data.clone(), Some(true), Some(true), ) @@ -184,114 +161,27 @@ pub async fn profile_create_from_duplicate( // Copy it over using the import system (essentially importing from the same profile) let state = State::get().await?; let bar = pack::import::copy_dotminecraft( - profile_path_id.clone(), - copy_from.get_full_path().await?, + &profile_path_id, + profile::get_full_path(copy_from).await?, &state.io_semaphore, None, ) .await?; let duplicated_profile = - profile::get(&profile_path_id, None).await?.ok_or_else(|| { + profile::get(&profile_path_id).await?.ok_or_else(|| { ErrorKind::UnmanagedProfileError(profile_path_id.to_string()) })?; crate::launcher::install_minecraft(&duplicated_profile, Some(bar), false) .await?; - { - let state = State::get().await?; - let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs( - &profile.get_profile_full_path().await?, - &mut file_watcher, - ) - .await?; - } // emit profile edited - emit_profile( - profile.uuid, - &profile.profile_id(), - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; - State::sync().await?; + emit_profile(&profile.path, &profile.name, ProfilePayloadType::Edited) + .await?; Ok(profile_path_id) } -#[tracing::instrument] -#[theseus_macros::debug_pin] -pub(crate) async fn get_loader_version_from_loader( - game_version: String, - loader: ModLoader, - loader_version: Option, -) -> crate::Result> { - let state = State::get().await?; - let metadata = state.metadata.read().await; - - let version = loader_version.unwrap_or_else(|| "latest".to_string()); - - let filter = |it: &LoaderVersion| match version.as_str() { - "latest" => true, - "stable" => it.stable, - id => { - it.id == *id - || format!("{}-{}", game_version, id) == it.id - || format!("{}-{}-{}", game_version, id, game_version) == it.id - } - }; - - let loader_data = match loader { - ModLoader::Forge => &metadata.forge, - ModLoader::Fabric => &metadata.fabric, - ModLoader::Quilt => &metadata.quilt, - ModLoader::NeoForge => &metadata.neoforge, - _ => { - return Err( - ProfileCreationError::NoManifest(loader.to_string()).into() - ) - } - }; - - let loaders = &loader_data - .game_versions - .iter() - .find(|it| { - it.id - .replace(daedalus::modded::DUMMY_REPLACE_STRING, &game_version) - == game_version - }) - .ok_or_else(|| { - ProfileCreationError::ModloaderUnsupported( - loader.to_string(), - game_version.clone(), - ) - })? - .loaders; - - let loader_version = loaders - .iter() - .find(|&x| filter(x)) - .cloned() - .or( - // If stable was searched for but not found, return latest by default - if version == "stable" { - loaders.iter().next().cloned() - } else { - None - }, - ) - .ok_or_else(|| { - ProfileCreationError::InvalidVersionModloader( - version, - loader.to_string(), - ) - })?; - - Ok(Some(loader_version)) -} - #[derive(thiserror::Error, Debug)] pub enum ProfileCreationError { #[error("Profile .json exists: {0}")] diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs index 47b83ccfa..dc7ca4da0 100644 --- a/packages/app-lib/src/api/profile/mod.rs +++ b/packages/app-lib/src/api/profile/mod.rs @@ -7,10 +7,7 @@ use crate::event::LoadingBarType; use crate::pack::install_from::{ EnvType, PackDependency, PackFile, PackFileHash, PackFormat, }; -use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; -use crate::state::{ - Credentials, InnerProjectPathUnix, ProjectMetadata, SideType, -}; +use crate::state::{CachedEntry, Credentials, JavaVersion, SideType}; use crate::util::fetch; use crate::util::io::{self, IOError}; @@ -18,16 +15,15 @@ use crate::{ event::{emit::emit_profile, ProfilePayloadType}, state::MinecraftChild, }; -pub use crate::{ - state::{JavaSettings, Profile}, - State, -}; +pub use crate::{state::Profile, State}; use async_zip::tokio::write::ZipFileWriter; use async_zip::{Compression, ZipEntryBuilder}; use serde_json::json; use std::collections::{HashMap, HashSet}; +use crate::data::Settings; +use dashmap::DashMap; use std::iter::FromIterator; use std::{ future::Future, @@ -42,205 +38,155 @@ pub mod update; /// Remove a profile #[tracing::instrument] -pub async fn remove(path: &ProfilePathId) -> crate::Result<()> { +pub async fn remove(path: &str) -> crate::Result<()> { let state = State::get().await?; - let mut profiles = state.profiles.write().await; - - if let Some(profile) = profiles.remove(path).await? { - emit_profile( - profile.uuid, - path, - &profile.metadata.name, - ProfilePayloadType::Removed, - ) - .await?; - } - Ok(()) -} + let mut transaction = state.pool.begin().await?; -/// Get a profile by relative path (or, name) -#[tracing::instrument] -pub async fn get( - path: &ProfilePathId, - clear_projects: Option, -) -> crate::Result> { - let state = State::get().await?; - let profiles = state.profiles.read().await; - let mut profile = profiles.0.get(path).cloned(); + let profile = Profile::get(path, &mut *transaction).await?; - if clear_projects.unwrap_or(false) { - if let Some(profile) = &mut profile { - profile.projects = HashMap::new(); - } + if let Some(profile) = profile { + profile.remove(&mut transaction).await?; + + emit_profile(path, &profile.name, ProfilePayloadType::Removed).await?; + + transaction.commit().await?; } - Ok(profile) + Ok(()) } -/// Get a profile by uuid +/// Get a profile by relative path (or, name) #[tracing::instrument] -pub async fn get_by_uuid( - uuid: uuid::Uuid, - clear_projects: Option, -) -> crate::Result> { +pub async fn get(path: &str) -> crate::Result> { let state = State::get().await?; - - let profiles = state.profiles.read().await; - let mut profile = profiles.0.values().find(|x| x.uuid == uuid).cloned(); - - if clear_projects.unwrap_or(false) { - if let Some(profile) = &mut profile { - profile.projects = HashMap::new(); - } - } + let profile = Profile::get(path, &state.pool).await?; Ok(profile) } /// Get profile's full path in the filesystem #[tracing::instrument] -pub async fn get_full_path(path: &ProfilePathId) -> crate::Result { - let _ = get(path, Some(true)).await?.ok_or_else(|| { - crate::ErrorKind::OtherError(format!( - "Tried to get the full path of a nonexistent or unloaded profile at path {}!", - path - )) - })?; - let full_path = io::canonicalize(path.get_full_path().await?)?; +pub async fn get_full_path(path: &str) -> crate::Result { + let state = State::get().await?; + let profiles_dir = state.directories.profiles_dir().await; + + let full_path = io::canonicalize(profiles_dir.join(&path))?; Ok(full_path) } /// Get mod's full path in the filesystem -#[tracing::instrument] -pub async fn get_mod_full_path( - profile_path: &ProfilePathId, - project_path: &ProjectPathId, -) -> crate::Result { - if get(profile_path, Some(true)).await?.is_some() { - let full_path = io::canonicalize( - project_path.get_full_path(profile_path.clone()).await?, - )?; - return Ok(full_path); - } - - Err(crate::ErrorKind::OtherError(format!( - "Tried to get the full path of a nonexistent or unloaded project at path {}!", - project_path.get_full_path(profile_path.clone()).await?.display() - )) - .into()) -} +// #[tracing::instrument] +// pub async fn get_mod_full_path( +// profile_path: &ProfilePathId, +// project_path: &ProjectPathId, +// ) -> crate::Result { +// if get(profile_path).await?.is_some() { +// let full_path = io::canonicalize( +// project_path.get_full_path(profile_path.clone()).await?, +// )?; +// return Ok(full_path); +// } +// +// Err(crate::ErrorKind::OtherError(format!( +// "Tried to get the full path of a nonexistent or unloaded project at path {}!", +// project_path.get_full_path(profile_path.clone()).await?.display() +// )) +// .into()) +// } /// Edit a profile using a given asynchronous closure pub async fn edit( - path: &ProfilePathId, + path: &str, action: impl Fn(&mut Profile) -> Fut, ) -> crate::Result<()> where Fut: Future>, { let state = State::get().await?; - let mut profiles = state.profiles.write().await; - match profiles.0.get_mut(path) { - Some(ref mut profile) => { - action(profile).await?; + if let Some(mut profile) = get(path).await? { + action(&mut profile).await?; + profile.upsert(&state.pool).await?; - emit_profile( - profile.uuid, - path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; + emit_profile(path, &profile.name, ProfilePayloadType::Edited).await?; - Ok(()) - } - None => Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) - .as_error()), + Ok(()) + } else { + Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) + .as_error()) } } /// Edits a profile's icon pub async fn edit_icon( - path: &ProfilePathId, + path: &str, icon_path: Option<&Path>, ) -> crate::Result<()> { let state = State::get().await?; - let res = if let Some(icon) = icon_path { - let bytes = io::read(icon).await?; - - let mut profiles = state.profiles.write().await; - - match profiles.0.get_mut(path) { - Some(ref mut profile) => { - profile - .set_icon( - &state.directories.caches_dir(), - &state.io_semaphore, - bytes::Bytes::from(bytes), - &icon.to_string_lossy(), - ) - .await?; - - emit_profile( - profile.uuid, - path, - &profile.metadata.name, - ProfilePayloadType::Edited, + if let Some(mut profile) = get(path).await? { + if let Some(icon) = icon_path { + let bytes = io::read(icon).await?; + + profile + .set_icon( + &state.directories.caches_dir(), + &state.io_semaphore, + bytes::Bytes::from(bytes), + &icon.to_string_lossy(), ) .await?; - Ok(()) - } - None => { - Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) - .as_error()) - } + } else { + profile.icon_path = None; } - } else { - edit(path, |profile| { - profile.metadata.icon = None; - async { Ok(()) } - }) - .await?; - State::sync().await?; + + profile.upsert(&state.pool).await?; + + emit_profile(path, &profile.name, ProfilePayloadType::Edited).await?; Ok(()) - }; - State::sync().await?; - res + } else { + Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) + .as_error()) + } } // Gets the optimal JRE key for the given profile, using Daedalus // Generally this would be used for profile_create, to get the optimal JRE key // this can be overwritten by the user a profile-by-profile basis pub async fn get_optimal_jre_key( - path: &ProfilePathId, + path: &str, ) -> crate::Result> { let state = State::get().await?; - if let Some(profile) = get(path, None).await? { - let metadata = state.metadata.read().await; + if let Some(profile) = get(path).await? { + let minecraft = crate::api::metadata::get_minecraft_versions().await?; // Fetch version info from stored profile game_version - let version = metadata - .minecraft + let version = minecraft .versions .iter() - .find(|it| it.id == profile.metadata.game_version) + .find(|it| it.id == profile.game_version) .ok_or_else(|| { crate::ErrorKind::LauncherError(format!( "Invalid or unknown Minecraft version: {}", - profile.metadata.game_version + profile.game_version )) })?; + let loader_version = crate::launcher::get_loader_version_from_profile( + &profile.game_version, + profile.loader, + profile.loader_version.as_deref(), + ) + .await?; + // Get detailed manifest info from Daedalus let version_info = crate::launcher::download::download_version_info( &state, version, - profile.metadata.loader_version.as_ref(), + loader_version.as_ref(), None, None, ) @@ -261,425 +207,411 @@ pub async fn get_optimal_jre_key( /// Get a copy of the profile set #[tracing::instrument] -pub async fn list( - clear_projects: Option, -) -> crate::Result> { +pub async fn list() -> crate::Result> { let state = State::get().await?; - let profiles = state.profiles.read().await; - Ok(profiles - .0 - .clone() - .into_iter() - .map(|mut x| { - if clear_projects.unwrap_or(false) { - x.1.projects = HashMap::new(); - } - - x - }) - .collect()) + let profiles = Profile::get_all(&state.pool).await?; + Ok(profiles) } /// Installs/Repairs a profile #[tracing::instrument] -pub async fn install(path: &ProfilePathId, force: bool) -> crate::Result<()> { - if let Some(profile) = get(path, None).await? { - crate::launcher::install_minecraft(&profile, None, force).await?; +pub async fn install(path: &str, force: bool) -> crate::Result<()> { + if let Some(mut profile) = get(path).await? { + crate::launcher::install_minecraft(&mut profile, None, force).await?; } else { return Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) .as_error()); } - State::sync().await?; Ok(()) } -#[tracing::instrument] -#[theseus_macros::debug_pin] -pub async fn update_all_projects( - profile_path: &ProfilePathId, -) -> crate::Result> { - if let Some(profile) = get(profile_path, None).await? { - let loading_bar = init_loading( - LoadingBarType::ProfileUpdate { - profile_path: profile.get_profile_full_path().await?, - profile_name: profile.metadata.name.clone(), - }, - 100.0, - "Updating profile", - ) - .await?; - - let keys = profile - .projects - .into_iter() - .filter(|(_, project)| { - matches!( - &project.metadata, - ProjectMetadata::Modrinth { - update_version: Some(_), - .. - } - ) - }) - .map(|x| x.0) - .collect::>(); - let len = keys.len(); - - let map = Arc::new(RwLock::new(HashMap::new())); - - use futures::StreamExt; - loading_try_for_each_concurrent( - futures::stream::iter(keys).map(Ok::), - None, - Some(&loading_bar), - 100.0, - len, - None, - |project| async { - let map = map.clone(); - - async move { - let new_path = - update_project(profile_path, &project, Some(true)) - .await?; - - map.write().await.insert(project, new_path); - - Ok(()) - } - .await - }, - ) - .await?; - - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; - State::sync().await?; - - Ok(Arc::try_unwrap(map).unwrap().into_inner()) - } else { - Err( - crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) - .as_error(), - ) - } -} +// #[tracing::instrument] +// #[theseus_macros::debug_pin] +// pub async fn update_all_projects( +// profile_path: &ProfilePathId, +// ) -> crate::Result> { +// if let Some(profile) = get(profile_path, None).await? { +// let loading_bar = init_loading( +// LoadingBarType::ProfileUpdate { +// profile_path: profile.get_profile_full_path().await?, +// profile_name: profile.metadata.name.clone(), +// }, +// 100.0, +// "Updating profile", +// ) +// .await?; +// +// let keys = profile +// .projects +// .into_iter() +// .filter(|(_, project)| { +// matches!( +// &project.metadata, +// ProjectMetadata::Modrinth { +// update_version: Some(_), +// .. +// } +// ) +// }) +// .map(|x| x.0) +// .collect::>(); +// let len = keys.len(); +// +// let map = Arc::new(RwLock::new(HashMap::new())); +// +// use futures::StreamExt; +// loading_try_for_each_concurrent( +// futures::stream::iter(keys).map(Ok::), +// None, +// Some(&loading_bar), +// 100.0, +// len, +// None, +// |project| async { +// let map = map.clone(); +// +// async move { +// let new_path = +// update_project(profile_path, &project, Some(true)) +// .await?; +// +// map.write().await.insert(project, new_path); +// +// Ok(()) +// } +// .await +// }, +// ) +// .await?; +// +// emit_profile( +// profile.uuid, +// profile_path, +// &profile.metadata.name, +// ProfilePayloadType::Edited, +// ) +// .await?; +// State::sync().await?; +// +// Ok(Arc::try_unwrap(map).unwrap().into_inner()) +// } else { +// Err( +// crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) +// .as_error(), +// ) +// } +// } /// Updates a project to the latest version /// Uses and returns the relative path to the project -#[tracing::instrument] -#[theseus_macros::debug_pin] -pub async fn update_project( - profile_path: &ProfilePathId, - project_path: &ProjectPathId, - skip_send_event: Option, -) -> crate::Result { - if let Some(profile) = get(profile_path, None).await? { - if let Some(project) = profile.projects.get(project_path) { - if let ProjectMetadata::Modrinth { - update_version: Some(update_version), - .. - } = &project.metadata - { - let (path, new_version) = profile - .add_project_version(update_version.id.clone()) - .await?; - - if project.disabled { - profile.toggle_disable_project(&path).await?; - } - - if path != project_path.clone() { - profile.remove_project(project_path, Some(true)).await?; - } - - let state = State::get().await?; - let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(profile_path) { - let value = profile.projects.remove(project_path); - if let Some(mut project) = value { - if let ProjectMetadata::Modrinth { - ref mut version, - ref mut update_version, - .. - } = project.metadata - { - *version = Box::new(new_version); - *update_version = None; - } - profile.projects.insert(path.clone(), project); - } - } - drop(profiles); - - if !skip_send_event.unwrap_or(false) { - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; - State::sync().await?; - } - - return Ok(path); - } - } - - Err(crate::ErrorKind::InputError( - "This project cannot be updated!".to_string(), - ) - .as_error()) - } else { - Err( - crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) - .as_error(), - ) - } -} +// #[tracing::instrument] +// #[theseus_macros::debug_pin] +// pub async fn update_project( +// profile_path: &ProfilePathId, +// project_path: &ProjectPathId, +// skip_send_event: Option, +// ) -> crate::Result { +// if let Some(profile) = get(profile_path, None).await? { +// if let Some(project) = profile.projects.get(project_path) { +// if let ProjectMetadata::Modrinth { +// update_version: Some(update_version), +// .. +// } = &project.metadata +// { +// let (path, new_version) = profile +// .add_project_version(update_version.id.clone()) +// .await?; +// +// if project.disabled { +// profile.toggle_disable_project(&path).await?; +// } +// +// if path != project_path.clone() { +// profile.remove_project(project_path, Some(true)).await?; +// } +// +// let state = State::get().await?; +// let mut profiles = state.profiles.write().await; +// if let Some(profile) = profiles.0.get_mut(profile_path) { +// let value = profile.projects.remove(project_path); +// if let Some(mut project) = value { +// if let ProjectMetadata::Modrinth { +// ref mut version, +// ref mut update_version, +// .. +// } = project.metadata +// { +// *version = Box::new(new_version); +// *update_version = None; +// } +// profile.projects.insert(path.clone(), project); +// } +// } +// drop(profiles); +// +// if !skip_send_event.unwrap_or(false) { +// emit_profile( +// profile.uuid, +// profile_path, +// &profile.metadata.name, +// ProfilePayloadType::Edited, +// ) +// .await?; +// State::sync().await?; +// } +// +// return Ok(path); +// } +// } +// +// Err(crate::ErrorKind::InputError( +// "This project cannot be updated!".to_string(), +// ) +// .as_error()) +// } else { +// Err( +// crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) +// .as_error(), +// ) +// } +// } /// Add a project from a version /// Returns the relative path to the project as a ProjectPathId -#[tracing::instrument] -pub async fn add_project_from_version( - profile_path: &ProfilePathId, - version_id: String, -) -> crate::Result { - if let Some(profile) = get(profile_path, None).await? { - let (project_path, _) = profile.add_project_version(version_id).await?; - - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; - Ok(project_path) - } else { - Err( - crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) - .as_error(), - ) - } -} +// #[tracing::instrument] +// pub async fn add_project_from_version( +// profile_path: &ProfilePathId, +// version_id: String, +// ) -> crate::Result { +// if let Some(profile) = get(profile_path).await? { +// let (project_path, _) = profile.add_project_version(version_id).await?; +// +// emit_profile( +// profile.uuid, +// profile_path, +// &profile.metadata.name, +// ProfilePayloadType::Edited, +// ) +// .await?; +// Ok(project_path) +// } else { +// Err( +// crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) +// .as_error(), +// ) +// } +// } /// Add a project from an FS path /// Uses and returns the relative path to the project as a ProjectPathId -#[tracing::instrument] -pub async fn add_project_from_path( - profile_path: &ProfilePathId, - path: &Path, - project_type: Option, -) -> crate::Result { - if let Some(profile) = get(profile_path, None).await? { - let file = io::read(path).await?; - let file_name = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - let path = profile - .add_project_bytes( - &file_name, - bytes::Bytes::from(file), - project_type.and_then(|x| serde_json::from_str(&x).ok()), - ) - .await?; - - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; - State::sync().await?; - - Ok(path) - } else { - Err( - crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) - .as_error(), - ) - } -} +// #[tracing::instrument] +// pub async fn add_project_from_path( +// profile_path: &ProfilePathId, +// path: &Path, +// project_type: Option, +// ) -> crate::Result { +// if let Some(profile) = get(profile_path).await? { +// let file = io::read(path).await?; +// let file_name = path +// .file_name() +// .unwrap_or_default() +// .to_string_lossy() +// .to_string(); +// +// let path = profile +// .add_project_bytes( +// &file_name, +// bytes::Bytes::from(file), +// project_type.and_then(|x| serde_json::from_str(&x).ok()), +// ) +// .await?; +// +// emit_profile( +// profile.uuid, +// profile_path, +// &profile.metadata.name, +// ProfilePayloadType::Edited, +// ) +// .await?; +// State::sync().await?; +// +// Ok(path) +// } else { +// Err( +// crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) +// .as_error(), +// ) +// } +// } /// Toggle whether a project is disabled or not /// Project path should be relative to the profile /// returns the new state, relative to the profile -#[tracing::instrument] -pub async fn toggle_disable_project( - profile_path: &ProfilePathId, - project: &ProjectPathId, -) -> crate::Result { - if let Some(profile) = get(profile_path, None).await? { - let res = profile.toggle_disable_project(project).await?; - - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; - State::sync().await?; - - Ok(res) - } else { - Err( - crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) - .as_error(), - ) - } -} +// #[tracing::instrument] +// pub async fn toggle_disable_project( +// profile_path: &ProfilePathId, +// project: &ProjectPathId, +// ) -> crate::Result { +// if let Some(profile) = get(profile_path).await? { +// let res = profile.toggle_disable_project(project).await?; +// +// emit_profile( +// profile.uuid, +// profile_path, +// &profile.metadata.name, +// ProfilePayloadType::Edited, +// ) +// .await?; +// State::sync().await?; +// +// Ok(res) +// } else { +// Err( +// crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) +// .as_error(), +// ) +// } +// } /// Remove a project from a profile /// Uses and returns the relative path to the project -#[tracing::instrument] -pub async fn remove_project( - profile_path: &ProfilePathId, - project: &ProjectPathId, -) -> crate::Result<()> { - if let Some(profile) = get(profile_path, None).await? { - profile.remove_project(project, None).await?; - - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; - State::sync().await?; - - Ok(()) - } else { - Err( - crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) - .as_error(), - ) - } -} +// #[tracing::instrument] +// pub async fn remove_project( +// profile_path: &ProfilePathId, +// project: &ProjectPathId, +// ) -> crate::Result<()> { +// if let Some(profile) = get(profile_path, None).await? { +// profile.remove_project(project, None).await?; +// +// emit_profile( +// profile.uuid, +// profile_path, +// &profile.metadata.name, +// ProfilePayloadType::Edited, +// ) +// .await?; +// State::sync().await?; +// +// Ok(()) +// } else { +// Err( +// crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) +// .as_error(), +// ) +// } +// } /// Exports the profile to a Modrinth-formatted .mrpack file // Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) -#[tracing::instrument(skip_all)] -#[theseus_macros::debug_pin] -pub async fn export_mrpack( - profile_path: &ProfilePathId, - export_path: PathBuf, - included_export_candidates: Vec, // which folders/files to include in the export - version_id: Option, - description: Option, - _name: Option, -) -> crate::Result<()> { - let state = State::get().await?; - let io_semaphore = state.io_semaphore.0.read().await; - let _permit: tokio::sync::SemaphorePermit = io_semaphore.acquire().await?; - let profile = get(profile_path, None).await?.ok_or_else(|| { - crate::ErrorKind::OtherError(format!( - "Tried to export a nonexistent or unloaded profile at path {}!", - profile_path - )) - })?; - - // remove .DS_Store files from included_export_candidates - let included_export_candidates = included_export_candidates - .into_iter() - .filter(|x| { - if let Some(f) = PathBuf::from(x).file_name() { - if f.to_string_lossy().starts_with(".DS_Store") { - return false; - } - } - true - }) - .collect::>(); - - let profile_base_path = &profile.get_profile_full_path().await?; - - let mut file = File::create(&export_path) - .await - .map_err(|e| IOError::with_path(e, &export_path))?; - let mut writer = ZipFileWriter::with_tokio(&mut file); - - // Create mrpack json configuration file - let version_id = version_id.unwrap_or("1.0.0".to_string()); - let mut packfile = - create_mrpack_json(&profile, version_id, description).await?; - let included_candidates_set = - HashSet::<_>::from_iter(included_export_candidates.iter()); - packfile.files.retain(|f| { - included_candidates_set.contains(&f.path.get_topmost_two_components()) - }); - - // Build vec of all files in the folder - let mut path_list = Vec::new(); - add_all_recursive_folder_paths(profile_base_path, &mut path_list).await?; - - // Initialize loading bar - let loading_bar = init_loading( - LoadingBarType::ZipExtract { - profile_path: profile.get_profile_full_path().await?, - profile_name: profile.metadata.name.clone(), - }, - path_list.len() as f64, - "Exporting profile to .mrpack", - ) - .await?; - - // Iterate over every file in the folder - // Every file that is NOT in the config file is added to the zip, in overrides - for path in path_list { - emit_loading(&loading_bar, 1.0, None).await?; - - let relative_path = ProjectPathId::from_fs_path(&path) - .await? - .get_inner_path_unix(); - if packfile.files.iter().any(|f| f.path == relative_path) - || !included_candidates_set - .contains(&relative_path.get_topmost_two_components()) - { - continue; - } - - // File is not in the config file, add it to the .mrpack zip - if path.is_file() { - let mut file = File::open(&path) - .await - .map_err(|e| IOError::with_path(e, &path))?; - let mut data = Vec::new(); - file.read_to_end(&mut data) - .await - .map_err(|e| IOError::with_path(e, &path))?; - let builder = ZipEntryBuilder::new( - format!("overrides/{relative_path}").into(), - Compression::Deflate, - ); - writer.write_entry_whole(builder, &data).await?; - } - } - - // Add modrinth json to the zip - let data = serde_json::to_vec_pretty(&packfile)?; - let builder = ZipEntryBuilder::new( - "modrinth.index.json".to_string().into(), - Compression::Deflate, - ); - writer.write_entry_whole(builder, &data).await?; - - writer.close().await?; - - Ok(()) -} +// #[tracing::instrument(skip_all)] +// #[theseus_macros::debug_pin] +// pub async fn export_mrpack( +// profile_path: &str, +// export_path: PathBuf, +// included_export_candidates: Vec, // which folders/files to include in the export +// version_id: Option, +// description: Option, +// _name: Option, +// ) -> crate::Result<()> { +// let state = State::get().await?; +// let io_semaphore = state.io_semaphore.0.read().await; +// let _permit: tokio::sync::SemaphorePermit = io_semaphore.acquire().await?; +// let profile = get(profile_path).await?.ok_or_else(|| { +// crate::ErrorKind::OtherError(format!( +// "Tried to export a nonexistent or unloaded profile at path {}!", +// profile_path +// )) +// })?; +// +// // remove .DS_Store files from included_export_candidates +// let included_export_candidates = included_export_candidates +// .into_iter() +// .filter(|x| { +// if let Some(f) = PathBuf::from(x).file_name() { +// if f.to_string_lossy().starts_with(".DS_Store") { +// return false; +// } +// } +// true +// }) +// .collect::>(); +// +// let profile_base_path = &profile.get_profile_full_path().await?; +// +// let mut file = File::create(&export_path) +// .await +// .map_err(|e| IOError::with_path(e, &export_path))?; +// let mut writer = ZipFileWriter::with_tokio(&mut file); +// +// // Create mrpack json configuration file +// let version_id = version_id.unwrap_or("1.0.0".to_string()); +// let mut packfile = +// create_mrpack_json(&profile, version_id, description).await?; +// let included_candidates_set = +// HashSet::<_>::from_iter(included_export_candidates.iter()); +// packfile.files.retain(|f| { +// included_candidates_set.contains(&f.path.get_topmost_two_components()) +// }); +// +// // Build vec of all files in the folder +// let mut path_list = Vec::new(); +// add_all_recursive_folder_paths(profile_base_path, &mut path_list).await?; +// +// // Initialize loading bar +// let loading_bar = init_loading( +// LoadingBarType::ZipExtract { +// profile_path: profile.get_profile_full_path().await?, +// profile_name: profile.name.clone(), +// }, +// path_list.len() as f64, +// "Exporting profile to .mrpack", +// ) +// .await?; +// +// // Iterate over every file in the folder +// // Every file that is NOT in the config file is added to the zip, in overrides +// for path in path_list { +// emit_loading(&loading_bar, 1.0, None).await?; +// +// let relative_path = ProjectPathId::from_fs_path(&path) +// .await? +// .get_inner_path_unix(); +// if packfile.files.iter().any(|f| f.path == relative_path) +// || !included_candidates_set +// .contains(&relative_path.get_topmost_two_components()) +// { +// continue; +// } +// +// // File is not in the config file, add it to the .mrpack zip +// if path.is_file() { +// let mut file = File::open(&path) +// .await +// .map_err(|e| IOError::with_path(e, &path))?; +// let mut data = Vec::new(); +// file.read_to_end(&mut data) +// .await +// .map_err(|e| IOError::with_path(e, &path))?; +// let builder = ZipEntryBuilder::new( +// format!("overrides/{relative_path}").into(), +// Compression::Deflate, +// ); +// writer.write_entry_whole(builder, &data).await?; +// } +// } +// +// // Add modrinth json to the zip +// let data = serde_json::to_vec_pretty(&packfile)?; +// let builder = ZipEntryBuilder::new( +// "modrinth.index.json".to_string().into(), +// Compression::Deflate, +// ); +// writer.write_entry_whole(builder, &data).await?; +// +// writer.close().await?; +// +// Ok(()) +// } // Given a folder path, populate a Vec of all the subfolders and files, at most 2 layers deep // profile @@ -690,59 +622,57 @@ pub async fn export_mrpack( // -- folder2file // -- file1 // => [folder1, folder2/innerfolder, folder2/folder2file, file1] -#[tracing::instrument] -pub async fn get_pack_export_candidates( - profile_path: &ProfilePathId, -) -> crate::Result> { - // First, get a dummy mrpack json for the files within - let profile: Profile = get(profile_path, None).await?.ok_or_else(|| { - crate::ErrorKind::OtherError(format!( - "Tried to export a nonexistent or unloaded profile at path {}!", - profile_path - )) - })?; - - let mut path_list: Vec = Vec::new(); - - let profile_base_dir = profile.get_profile_full_path().await?; - let mut read_dir = io::read_dir(&profile_base_dir).await?; - while let Some(entry) = read_dir - .next_entry() - .await - .map_err(|e| IOError::with_path(e, &profile_base_dir))? - { - let path: PathBuf = entry.path(); - if path.is_dir() { - // Two layers of files/folders if its a folder - let mut read_dir = io::read_dir(&path).await?; - while let Some(entry) = read_dir - .next_entry() - .await - .map_err(|e| IOError::with_path(e, &profile_base_dir))? - { - let path: PathBuf = entry.path(); - if let Ok(project_path) = - ProjectPathId::from_fs_path(&path).await - { - path_list.push(project_path.get_inner_path_unix()); - } - } - } else { - // One layer of files/folders if its a file - if let Ok(project_path) = ProjectPathId::from_fs_path(&path).await { - path_list.push(project_path.get_inner_path_unix()); - } - } - } - Ok(path_list) -} +// #[tracing::instrument] +// pub async fn get_pack_export_candidates( +// profile_path: &ProfilePathId, +// ) -> crate::Result> { +// // First, get a dummy mrpack json for the files within +// let profile: Profile = get(profile_path).await?.ok_or_else(|| { +// crate::ErrorKind::OtherError(format!( +// "Tried to export a nonexistent or unloaded profile at path {}!", +// profile_path +// )) +// })?; +// +// let mut path_list: Vec = Vec::new(); +// +// let profile_base_dir = profile.get_profile_full_path().await?; +// let mut read_dir = io::read_dir(&profile_base_dir).await?; +// while let Some(entry) = read_dir +// .next_entry() +// .await +// .map_err(|e| IOError::with_path(e, &profile_base_dir))? +// { +// let path: PathBuf = entry.path(); +// if path.is_dir() { +// // Two layers of files/folders if its a folder +// let mut read_dir = io::read_dir(&path).await?; +// while let Some(entry) = read_dir +// .next_entry() +// .await +// .map_err(|e| IOError::with_path(e, &profile_base_dir))? +// { +// let path: PathBuf = entry.path(); +// if let Ok(project_path) = +// ProjectPathId::from_fs_path(&path).await +// { +// path_list.push(project_path.get_inner_path_unix()); +// } +// } +// } else { +// // One layer of files/folders if its a file +// if let Ok(project_path) = ProjectPathId::from_fs_path(&path).await { +// path_list.push(project_path.get_inner_path_unix()); +// } +// } +// } +// Ok(path_list) +// } /// Run Minecraft using a profile and the default credentials, logged in credentials, /// failing with an error if no credentials are available #[tracing::instrument] -pub async fn run( - path: &ProfilePathId, -) -> crate::Result>> { +pub async fn run(path: &str) -> crate::Result>> { let state = State::get().await?; // Get default account and refresh credentials (preferred way to log in) @@ -763,25 +693,28 @@ pub async fn run( #[tracing::instrument(skip(credentials))] #[theseus_macros::debug_pin] pub async fn run_credentials( - path: &ProfilePathId, + path: &str, credentials: &Credentials, ) -> crate::Result>> { let state = State::get().await?; - let settings = state.settings.read().await; - let profile = get(path, None).await?.ok_or_else(|| { + let settings = Settings::get(&state.pool).await?; + let mut profile = get(path).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to run a nonexistent or unloaded profile at path {}!", path )) })?; - let pre_launch_hooks = - &profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch; + let pre_launch_hooks = profile + .hooks + .pre_launch + .as_ref() + .or(settings.hooks.pre_launch.as_ref()); if let Some(hook) = pre_launch_hooks { // TODO: hook parameters let mut cmd = hook.split(' '); if let Some(command) = cmd.next() { - let full_path = path.get_full_path().await?; + let full_path = get_full_path(&profile.path).await?; let result = Command::new(command) .args(&cmd.collect::>()) .current_dir(&full_path) @@ -802,36 +735,28 @@ pub async fn run_credentials( } let java_args = profile - .java - .as_ref() - .and_then(|it| it.extra_arguments.as_ref()) - .unwrap_or(&settings.custom_java_args); + .extra_launch_args + .clone() + .unwrap_or(settings.extra_launch_args); - let wrapper = profile - .hooks - .as_ref() - .map_or(&settings.hooks.wrapper, |it| &it.wrapper); + let wrapper = profile.hooks.wrapper.clone().or(settings.hooks.wrapper); let memory = profile.memory.unwrap_or(settings.memory); - let resolution = profile.resolution.unwrap_or(settings.game_resolution); + let resolution = + profile.game_resolution.unwrap_or(settings.game_resolution); let env_args = profile - .java - .as_ref() - .and_then(|x| x.custom_env_args.as_ref()) - .unwrap_or(&settings.custom_env_args); + .custom_env_vars + .clone() + .unwrap_or(settings.custom_env_vars); // Post post exit hooks - let post_exit_hook = profile - .hooks - .as_ref() - .unwrap_or(&settings.hooks) - .post_exit - .clone(); + let post_exit_hook = + profile.hooks.post_exit.clone().or(settings.hooks.post_exit); // Any options.txt settings that we want set, add here let mut mc_set_options: Vec<(String, String)> = vec![]; - if let Some(fullscreen) = profile.fullscreen { + if let Some(fullscreen) = profile.force_fullscreen { // Profile fullscreen setting takes priority mc_set_options.push(("fullscreen".to_string(), fullscreen.to_string())); } else if settings.force_fullscreen { @@ -840,15 +765,15 @@ pub async fn run_credentials( } let mc_process = crate::launcher::launch_minecraft( - java_args, - env_args, + &*java_args, + &*env_args, &mc_set_options, - wrapper, + &wrapper, &memory, &resolution, credentials, post_exit_hook, - &profile, + &mut profile, ) .await?; Ok(mc_process) @@ -857,35 +782,36 @@ pub async fn run_credentials( /// Update playtime- sending a request to the server to update the playtime #[tracing::instrument] #[theseus_macros::debug_pin] -pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> { +pub async fn try_update_playtime(path: &str) -> crate::Result<()> { let state = State::get().await?; - let profile = get(path, None).await?.ok_or_else(|| { + let profile = get(path).await?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to update playtime for a nonexistent or unloaded profile at path {}!", path )) })?; - let updated_recent_playtime = profile.metadata.recent_time_played; + let updated_recent_playtime = profile.recent_time_played; let res = if updated_recent_playtime > 0 { // Create update struct to send to Labrinth let modrinth_pack_version_id = - profile.metadata.linked_data.and_then(|l| l.version_id); + profile.linked_data.as_ref().map(|l| l.version_id.clone()); let playtime_update_json = json!({ "seconds": updated_recent_playtime, - "loader": profile.metadata.loader.to_string(), - "game_version": profile.metadata.game_version, + "loader": profile.loader.as_str(), + "game_version": profile.game_version, "parent": modrinth_pack_version_id, }); // Copy this struct for every Modrinth project in the profile let mut hashmap: HashMap = HashMap::new(); - for (_, project) in profile.projects { - if let ProjectMetadata::Modrinth { version, .. } = project.metadata - { - hashmap.insert(version.id, playtime_update_json.clone()); - } - } + // TODO: fix + // for (_, project) in profile.projects { + // if let ProjectMetadata::Modrinth { version, .. } = project.metadata + // { + // hashmap.insert(version.id, playtime_update_json.clone()); + // } + // } let creds = state.credentials.read().await; fetch::post_json( @@ -901,14 +827,14 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> { // If successful, update the profile metadata to match submitted if res.is_ok() { - let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(path) { - profile.metadata.submitted_time_played += updated_recent_playtime; - profile.metadata.recent_time_played = 0; - } + edit(&profile.path, |prof| { + prof.submitted_time_played += updated_recent_playtime; + prof.recent_time_played = 0; + + async { Ok(()) } + }) + .await?; } - // Sync either way - State::sync().await?; res } @@ -923,21 +849,18 @@ pub async fn create_mrpack_json( ) -> crate::Result { // Add loader version to dependencies let mut dependencies = HashMap::new(); - match ( - profile.metadata.loader, - profile.metadata.loader_version.clone(), - ) { + match (profile.loader, profile.loader_version.clone()) { (crate::prelude::ModLoader::Forge, Some(v)) => { - dependencies.insert(PackDependency::Forge, v.id) + dependencies.insert(PackDependency::Forge, v) } (crate::prelude::ModLoader::NeoForge, Some(v)) => { - dependencies.insert(PackDependency::NeoForge, v.id) + dependencies.insert(PackDependency::NeoForge, v) } (crate::prelude::ModLoader::Fabric, Some(v)) => { - dependencies.insert(PackDependency::FabricLoader, v.id) + dependencies.insert(PackDependency::FabricLoader, v) } (crate::prelude::ModLoader::Quilt, Some(v)) => { - dependencies.insert(PackDependency::QuiltLoader, v.id) + dependencies.insert(PackDependency::QuiltLoader, v) } (crate::prelude::ModLoader::Vanilla, _) => None, _ => { @@ -947,114 +870,80 @@ pub async fn create_mrpack_json( .into()) } }; - dependencies.insert( - PackDependency::Minecraft, - profile.metadata.game_version.clone(), - ); - - // Converts a HashMap to a HashMap - // But the values are sanitized to only include the version number - let dependencies = dependencies - .into_iter() - .map(|(k, v)| (k, sanitize_loader_version_string(&v, k).to_string())) - .collect::>(); - - let files: Result, crate::ErrorKind> = profile - .projects - .iter() - .filter_map(|(mod_path, project)| { - let path = mod_path.get_inner_path_unix(); - - // Only Modrinth projects have a modrinth metadata field for the modrinth.json - Some(Ok(match project.metadata { - crate::prelude::ProjectMetadata::Modrinth { - ref version, - .. - } => { - let mut env = HashMap::new(); - // TODO: envtype should be a controllable option (in general or at least .mrpack exporting) - // For now, assume required. - // env.insert(EnvType::Client, project.client_side.clone()); - // env.insert(EnvType::Server, project.server_side.clone()); - env.insert(EnvType::Client, SideType::Required); - env.insert(EnvType::Server, SideType::Required); - - let primary_file = if let Some(primary_file) = - version.files.first() - { - primary_file - } else { - return Some(Err(crate::ErrorKind::OtherError( - format!("No primary file found for mod at: {path}"), - ))); - }; - - let file_size = primary_file.size; - let downloads = vec![primary_file.url.clone()]; - let hashes = primary_file - .hashes - .clone() - .into_iter() - .map(|(h1, h2)| (PackFileHash::from(h1), h2)) - .collect(); - - PackFile { - path, - hashes, - env: Some(env), - downloads, - file_size, - } - } - // Inferred files are skipped for the modrinth.json - crate::prelude::ProjectMetadata::Inferred { .. } => { - return None - } - // Unknown projects are skipped for the modrinth.json - crate::prelude::ProjectMetadata::Unknown => return None, - })) - }) - .collect(); - let files = files?; + dependencies + .insert(PackDependency::Minecraft, profile.game_version.clone()); + + // TODO: fix + // let files: Result, crate::ErrorKind> = profile + // .projects + // .iter() + // .filter_map(|(mod_path, project)| { + // let path = mod_path.get_inner_path_unix(); + // + // // Only Modrinth projects have a modrinth metadata field for the modrinth.json + // Some(Ok(match project.metadata { + // crate::prelude::ProjectMetadata::Modrinth { + // ref version, + // .. + // } => { + // let mut env = HashMap::new(); + // // TODO: envtype should be a controllable option (in general or at least .mrpack exporting) + // // For now, assume required. + // // env.insert(EnvType::Client, project.client_side.clone()); + // // env.insert(EnvType::Server, project.server_side.clone()); + // env.insert(EnvType::Client, SideType::Required); + // env.insert(EnvType::Server, SideType::Required); + // + // let primary_file = if let Some(primary_file) = + // version.files.first() + // { + // primary_file + // } else { + // return Some(Err(crate::ErrorKind::OtherError( + // format!("No primary file found for mod at: {path}"), + // ))); + // }; + // + // let file_size = primary_file.size; + // let downloads = vec![primary_file.url.clone()]; + // let hashes = primary_file + // .hashes + // .clone() + // .into_iter() + // .map(|(h1, h2)| (PackFileHash::from(h1), h2)) + // .collect(); + // + // PackFile { + // path, + // hashes, + // env: Some(env), + // downloads, + // file_size, + // } + // } + // // Inferred files are skipped for the modrinth.json + // crate::prelude::ProjectMetadata::Inferred { .. } => { + // return None + // } + // // Unknown projects are skipped for the modrinth.json + // crate::prelude::ProjectMetadata::Unknown => return None, + // })) + // }) + // .collect(); + // let files = files?; + let files = vec![]; Ok(PackFormat { game: "minecraft".to_string(), format_version: 1, version_id, - name: profile.metadata.name.clone(), + name: profile.name.clone(), summary: description, files, dependencies, }) } -fn sanitize_loader_version_string(s: &str, loader: PackDependency) -> &str { - match loader { - // Split on '-' - // If two or more, take the second - // If one, take the first - // If none, take the whole thing - PackDependency::Forge | PackDependency::NeoForge => { - if s.starts_with("1.") { - let mut split: std::str::Split<'_, char> = s.split('-'); - match split.next() { - Some(first) => match split.next() { - Some(second) => second, - None => first, - }, - None => s, - } - } else { - s - } - } - // For quilt, etc we take the whole thing, as it functions like: 0.20.0-beta.11 (and should not be split here) - PackDependency::QuiltLoader - | PackDependency::FabricLoader - | PackDependency::Minecraft => s, - } -} - // Given a folder path, populate a Vec of all the files in the folder, recursively #[async_recursion::async_recursion] pub async fn add_all_recursive_folder_paths( diff --git a/packages/app-lib/src/api/profile/update.rs b/packages/app-lib/src/api/profile/update.rs index e46ceb068..db6399c15 100644 --- a/packages/app-lib/src/api/profile/update.rs +++ b/packages/app-lib/src/api/profile/update.rs @@ -1,13 +1,12 @@ use crate::{ event::{ - emit::{emit_profile, init_loading, loading_try_for_each_concurrent}, + emit::{emit_profile, init_loading}, ProfilePayloadType, }, pack::{self, install_from::generate_pack_from_version_id}, - prelude::{ProfilePathId, ProjectPathId}, profile::get, - state::{ProfileInstallStage, Project}, - LoadingBarType, State, + state::ProfileInstallStage, + LoadingBarType, }; use futures::try_join; @@ -15,10 +14,10 @@ use futures::try_join; #[tracing::instrument] #[theseus_macros::debug_pin] pub async fn update_managed_modrinth_version( - profile_path: &ProfilePathId, + profile_path: &String, new_version_id: &String, ) -> crate::Result<()> { - let profile = get(profile_path, None).await?.ok_or_else(|| { + let profile = get(profile_path).await?.ok_or_else(|| { crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) .as_error() })?; @@ -30,46 +29,30 @@ pub async fn update_managed_modrinth_version( }; // Extract modrinth pack information, if appropriate - let linked_data = profile - .metadata - .linked_data - .as_ref() - .ok_or_else(unmanaged_err)?; - let project_id: &String = - linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?; - let version_id = - linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?; + let linked_data = profile.linked_data.as_ref().ok_or_else(unmanaged_err)?; // Replace the pack with the new version replace_managed_modrinth( profile_path, &profile, - project_id, - version_id, + &linked_data.project_id, + &linked_data.version_id, Some(new_version_id), true, // switching versions should ignore the lock ) .await?; - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; + emit_profile(profile_path, &profile.name, ProfilePayloadType::Edited) + .await?; - State::sync().await?; Ok(()) } /// Repair a managed modrinth pack by 'updating' it to the current version #[tracing::instrument] #[theseus_macros::debug_pin] -pub async fn repair_managed_modrinth( - profile_path: &ProfilePathId, -) -> crate::Result<()> { - let profile = get(profile_path, None).await?.ok_or_else(|| { +pub async fn repair_managed_modrinth(profile_path: &str) -> crate::Result<()> { + let profile = get(profile_path).await?.ok_or_else(|| { crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) .as_error() })?; @@ -83,60 +66,47 @@ pub async fn repair_managed_modrinth( // For repairing specifically, first we remove all installed projects (to ensure we do remove ones that aren't in the pack) // We do a project removal followed by removing everything in the .mrpack, to ensure we only // remove relevant projects and not things like save files - let projects_map = profile.projects.clone(); - let stream = futures::stream::iter( - projects_map - .into_iter() - .map(Ok::<(ProjectPathId, Project), crate::Error>), - ); - loading_try_for_each_concurrent( - stream, - None, - None, - 0.0, - 0, - None, - |(project_id, _)| { - let profile = profile.clone(); - async move { - profile.remove_project(&project_id, Some(true)).await?; - Ok(()) - } - }, - ) - .await?; + // TODO: fix + // let projects_map = profile.projects.clone(); + // let stream = futures::stream::iter( + // projects_map + // .into_iter() + // .map(Ok::<(ProjectPathId, Project), crate::Error>), + // ); + // loading_try_for_each_concurrent( + // stream, + // None, + // None, + // 0.0, + // 0, + // None, + // |(project_id, _)| { + // let profile = profile.clone(); + // async move { + // profile.remove_project(&project_id, Some(true)).await?; + // Ok(()) + // } + // }, + // ) + // .await?; // Extract modrinth pack information, if appropriate - let linked_data = profile - .metadata - .linked_data - .as_ref() - .ok_or_else(unmanaged_err)?; - let project_id: &String = - linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?; - let version_id = - linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?; + let linked_data = profile.linked_data.as_ref().ok_or_else(unmanaged_err)?; // Replace the pack with the same version replace_managed_modrinth( profile_path, &profile, - project_id, - version_id, + &linked_data.project_id, + &linked_data.version_id, None, false, // do not ignore lock, as repairing can reset the lock ) .await?; - emit_profile( - profile.uuid, - profile_path, - &profile.metadata.name, - ProfilePayloadType::Edited, - ) - .await?; + emit_profile(profile_path, &profile.name, ProfilePayloadType::Edited) + .await?; - State::sync().await?; Ok(()) } @@ -145,7 +115,7 @@ pub async fn repair_managed_modrinth( #[tracing::instrument(skip(profile))] #[theseus_macros::debug_pin] async fn replace_managed_modrinth( - profile_path: &ProfilePathId, + profile_path: &str, profile: &crate::state::Profile, project_id: &String, version_id: &String, @@ -161,59 +131,63 @@ async fn replace_managed_modrinth( // Fetch .mrpacks for both old and new versions // TODO: this will need to be updated if we revert the hacky pack method we needed for compiler speed - let (old_pack_creator, new_pack_creator) = - if let Some(new_version_id) = new_version_id { - let shared_loading_bar = init_loading( - LoadingBarType::PackFileDownload { - profile_path: profile_path.get_full_path().await?, - pack_name: profile.metadata.name.clone(), - icon: None, - pack_version: version_id.clone(), - }, - 200.0, // These two downloads will share the same loading bar - "Downloading pack file", - ) - .await?; - - // download in parallel, then join. - try_join!( - generate_pack_from_version_id( - project_id.clone(), - version_id.clone(), - profile.metadata.name.clone(), - None, - profile_path.clone(), - Some(shared_loading_bar.clone()) - ), - generate_pack_from_version_id( - project_id.clone(), - new_version_id.clone(), - profile.metadata.name.clone(), - None, - profile_path.clone(), - Some(shared_loading_bar) - ) - )? - } else { - // If new_version_id is None, we don't need to download the new pack, so we clone the old one - let mut old_pack_creator = generate_pack_from_version_id( + let (old_pack_creator, new_pack_creator) = if let Some(new_version_id) = + new_version_id + { + let shared_loading_bar = init_loading( + LoadingBarType::PackFileDownload { + profile_path: crate::api::profile::get_full_path(profile_path) + .await? + .to_string_lossy() + .to_string(), + pack_name: profile.name.clone(), + icon: None, + pack_version: version_id.clone(), + }, + 200.0, // These two downloads will share the same loading bar + "Downloading pack file", + ) + .await?; + + // download in parallel, then join. + try_join!( + generate_pack_from_version_id( project_id.clone(), version_id.clone(), - profile.metadata.name.clone(), + profile.name.clone(), None, - profile_path.clone(), + profile_path.to_string(), + Some(shared_loading_bar.clone()) + ), + generate_pack_from_version_id( + project_id.clone(), + new_version_id.clone(), + profile.name.clone(), None, + profile_path.to_string(), + Some(shared_loading_bar) ) - .await?; - old_pack_creator.description.existing_loading_bar = None; - (old_pack_creator.clone(), old_pack_creator) - }; + )? + } else { + // If new_version_id is None, we don't need to download the new pack, so we clone the old one + let mut old_pack_creator = generate_pack_from_version_id( + project_id.clone(), + version_id.clone(), + profile.name.clone(), + None, + profile_path.to_string(), + None, + ) + .await?; + old_pack_creator.description.existing_loading_bar = None; + (old_pack_creator.clone(), old_pack_creator) + }; // Removal - remove all files that were added by the old pack // - remove all installed projects // - remove all overrides pack::install_mrpack::remove_all_related_files( - profile_path.clone(), + profile_path.to_string(), old_pack_creator.file, ) .await?; diff --git a/packages/app-lib/src/api/settings.rs b/packages/app-lib/src/api/settings.rs index 99edf487f..6f90595a9 100644 --- a/packages/app-lib/src/api/settings.rs +++ b/packages/app-lib/src/api/settings.rs @@ -1,21 +1,7 @@ //! Theseus profile management interface -use std::path::{Path, PathBuf}; -use tokio::fs; - -use io::IOError; -use tokio::sync::RwLock; - -use crate::{ - event::emit::{emit_loading, init_loading}, - prelude::DirectoryInfo, - state::{self, Profiles}, - util::{fetch, io}, -}; pub use crate::{ - state::{ - Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, - }, + state::{Hooks, MemorySettings, Profile, Settings, WindowSize}, State, }; @@ -23,224 +9,33 @@ pub use crate::{ #[tracing::instrument] pub async fn get() -> crate::Result { let state = State::get().await?; - let settings = state.settings.read().await; - Ok(settings.clone()) + let settings = Settings::get(&state.pool).await?; + Ok(settings) } /// Sets entire settings #[tracing::instrument] pub async fn set(settings: Settings) -> crate::Result<()> { let state = State::get().await?; + let old_settings = Settings::get(&state.pool).await?; - if settings.loaded_config_dir - != state.settings.read().await.loaded_config_dir - { - return Err(crate::ErrorKind::OtherError( - "Cannot change config directory as setting".to_string(), - ) - .as_error()); - } - - let (reset_io, reset_fetch) = async { - let read = state.settings.read().await; - ( - settings.max_concurrent_writes != read.max_concurrent_writes, - settings.max_concurrent_downloads != read.max_concurrent_downloads, - ) + if settings.max_concurrent_writes != old_settings.max_concurrent_writes { + let mut io_semaphore = state.io_semaphore.0.write().await; + *io_semaphore = + tokio::sync::Semaphore::new(settings.max_concurrent_writes); } - .await; - - let updated_discord_rpc = { - let read = state.settings.read().await; - settings.disable_discord_rpc != read.disable_discord_rpc - }; - + if settings.max_concurrent_downloads + != old_settings.max_concurrent_downloads { - *state.settings.write().await = settings; + let mut fetch_semaphore = state.fetch_semaphore.0.write().await; + *fetch_semaphore = + tokio::sync::Semaphore::new(settings.max_concurrent_downloads); } - - if updated_discord_rpc { + if settings.discord_rpc != old_settings.discord_rpc { state.discord_rpc.clear_to_default(true).await?; } - if reset_io { - state.reset_io_semaphore().await; - } - if reset_fetch { - state.reset_fetch_semaphore().await; - } + settings.update(&state.pool).await?; - State::sync().await?; Ok(()) } - -/// Sets the new config dir, the location of all Theseus data except for the settings.json and caches -/// Takes control of the entire state and blocks until completion -pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> { - tracing::trace!("Changing config dir to: {}", new_config_dir.display()); - if !new_config_dir.is_dir() { - return Err(crate::ErrorKind::FSError(format!( - "New config dir is not a folder: {}", - new_config_dir.display() - )) - .as_error()); - } - - if !is_dir_writeable(new_config_dir.clone()).await? { - return Err(crate::ErrorKind::FSError(format!( - "New config dir is not writeable: {}", - new_config_dir.display() - )) - .as_error()); - } - - let loading_bar = init_loading( - crate::LoadingBarType::ConfigChange { - new_path: new_config_dir.clone(), - }, - 100.0, - "Changing configuration directory", - ) - .await?; - - tracing::trace!("Changing config dir, taking control of the state"); - // Take control of the state - let mut state_write = State::get_write().await?; - let old_config_dir = - state_write.directories.config_dir.read().await.clone(); - - // Reset file watcher - tracing::trace!("Reset file watcher"); - let file_watcher = state::init_watcher().await?; - state_write.file_watcher = RwLock::new(file_watcher); - - // Getting files to be moved - let mut config_entries = io::read_dir(&old_config_dir).await?; - let across_drives = is_different_drive(&old_config_dir, &new_config_dir); - let mut entries = vec![]; - let mut deletable_entries = vec![]; - while let Some(entry) = config_entries - .next_entry() - .await - .map_err(|e| IOError::with_path(e, &old_config_dir))? - { - let entry_path = entry.path(); - if let Some(file_name) = entry_path.file_name() { - // We are only moving the profiles and metadata folders - if file_name == state::PROFILES_FOLDER_NAME - || file_name == state::METADATA_FOLDER_NAME - { - if across_drives { - entries.extend( - crate::pack::import::get_all_subfiles(&entry_path) - .await?, - ); - deletable_entries.push(entry_path.clone()); - } else { - entries.push(entry_path.clone()); - } - } - } - } - - tracing::trace!("Moving files"); - let semaphore = &state_write.io_semaphore; - let num_entries = entries.len() as f64; - for entry_path in entries { - let relative_path = entry_path.strip_prefix(&old_config_dir)?; - let new_path = new_config_dir.join(relative_path); - if across_drives { - fetch::copy(&entry_path, &new_path, semaphore).await?; - } else { - io::rename(entry_path.clone(), new_path.clone()).await?; - } - emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None).await?; - } - - tracing::trace!("Setting configuration setting"); - // Set load config dir setting - let settings = { - let mut settings = state_write.settings.write().await; - settings.loaded_config_dir = Some(new_config_dir.clone()); - - // Some java paths are hardcoded to within our config dir, so we need to update them - tracing::trace!("Updating java keys"); - for key in settings.java_globals.keys() { - if let Some(java) = settings.java_globals.get_mut(&key) { - // If the path is within the old config dir path, update it to the new config dir - if let Ok(relative_path) = PathBuf::from(java.path.clone()) - .strip_prefix(&old_config_dir) - { - java.path = new_config_dir - .join(relative_path) - .to_string_lossy() - .to_string(); - } - } - } - tracing::trace!("Syncing settings"); - - settings - .sync(&state_write.directories.settings_file()) - .await?; - settings.clone() - }; - - tracing::trace!("Reinitializing directory"); - // Set new state information - state_write.directories = DirectoryInfo::init(&settings)?; - - // Delete entries that were from a different drive - let deletable_entries_len = deletable_entries.len(); - if deletable_entries_len > 0 { - tracing::trace!("Deleting old files"); - } - for entry in deletable_entries { - io::remove_dir_all(entry).await?; - emit_loading( - &loading_bar, - 10.0 * (1.0 / deletable_entries_len as f64), - None, - ) - .await?; - } - - // Reset file watcher - tracing::trace!("Reset file watcher"); - let mut file_watcher = state::init_watcher().await?; - - // Reset profiles (for filepaths, file watcher, etc) - state_write.profiles = RwLock::new( - Profiles::init(&state_write.directories, &mut file_watcher).await?, - ); - state_write.file_watcher = RwLock::new(file_watcher); - - emit_loading(&loading_bar, 10.0, None).await?; - - tracing::info!( - "Successfully switched config folder to: {}", - new_config_dir.display() - ); - Ok(()) -} - -// Function to check if two paths are on different drives/roots -fn is_different_drive(path1: &Path, path2: &Path) -> bool { - let root1 = path1.components().next(); - let root2 = path2.components().next(); - root1 != root2 -} - -pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result { - let temp_path = new_config_dir.join(".tmp"); - match fs::write(temp_path.clone(), "test").await { - Ok(_) => { - fs::remove_file(temp_path).await?; - Ok(true) - } - Err(e) => { - tracing::error!("Error writing to new config dir: {}", e); - Ok(false) - } - } -} diff --git a/packages/app-lib/src/api/tags.rs b/packages/app-lib/src/api/tags.rs index 3ec2c9b97..cc0ceb68e 100644 --- a/packages/app-lib/src/api/tags.rs +++ b/packages/app-lib/src/api/tags.rs @@ -1,52 +1,70 @@ //! Theseus tag management interface +use crate::state::CachedEntry; pub use crate::{ - state::{Category, DonationPlatform, GameVersion, Loader, Tags}, + state::{Category, DonationPlatform, GameVersion, Loader}, State, }; -// Get bundled set of tags -#[tracing::instrument] -pub async fn get_tag_bundle() -> crate::Result { - let state = State::get().await?; - let tags = state.tags.read().await; - - Ok(tags.get_tag_bundle()) -} - /// Get category tags #[tracing::instrument] pub async fn get_category_tags() -> crate::Result> { let state = State::get().await?; - let tags = state.tags.read().await; + let categories = + CachedEntry::get_categories(None, &state.pool, &state.fetch_semaphore) + .await? + .ok_or_else(|| { + crate::ErrorKind::NoValueFor("category tags".to_string()) + })?; - Ok(tags.get_categories()) + Ok(categories) } /// Get report type tags #[tracing::instrument] pub async fn get_report_type_tags() -> crate::Result> { let state = State::get().await?; - let tags = state.tags.read().await; + let report_types = CachedEntry::get_report_types( + None, + &state.pool, + &state.fetch_semaphore, + ) + .await? + .ok_or_else(|| { + crate::ErrorKind::NoValueFor("report type tags".to_string()) + })?; - Ok(tags.get_report_types()) + Ok(report_types) } /// Get loader tags #[tracing::instrument] pub async fn get_loader_tags() -> crate::Result> { let state = State::get().await?; - let tags = state.tags.read().await; + let loaders = + CachedEntry::get_loaders(None, &state.pool, &state.fetch_semaphore) + .await? + .ok_or_else(|| { + crate::ErrorKind::NoValueFor("loader tags".to_string()) + })?; - Ok(tags.get_loaders()) + Ok(loaders) } /// Get game version tags #[tracing::instrument] pub async fn get_game_version_tags() -> crate::Result> { let state = State::get().await?; - let tags = state.tags.read().await; + let game_versions = CachedEntry::get_game_versions( + None, + &state.pool, + &state.fetch_semaphore, + ) + .await? + .ok_or_else(|| { + crate::ErrorKind::NoValueFor("game version tags".to_string()) + })?; - Ok(tags.get_game_versions()) + Ok(game_versions) } /// Get donation platform tags @@ -54,7 +72,15 @@ pub async fn get_game_version_tags() -> crate::Result> { pub async fn get_donation_platform_tags() -> crate::Result> { let state = State::get().await?; - let tags = state.tags.read().await; + let donation_platforms = CachedEntry::get_donation_platforms( + None, + &state.pool, + &state.fetch_semaphore, + ) + .await? + .ok_or_else(|| { + crate::ErrorKind::NoValueFor("donation platform tags".to_string()) + })?; - Ok(tags.get_donation_platforms()) + Ok(donation_platforms) } diff --git a/packages/app-lib/src/config.rs b/packages/app-lib/src/config.rs index 760f9eeda..aff704c2a 100644 --- a/packages/app-lib/src/config.rs +++ b/packages/app-lib/src/config.rs @@ -1,3 +1,6 @@ //! Configuration structs pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/"; +pub const MODRINTH_API_URL_V3: &str = "https://api.modrinth.com/v3/"; + +pub const META_URL: &str = "https://launcher-meta.modrinth.com/"; diff --git a/packages/app-lib/src/error.rs b/packages/app-lib/src/error.rs index b3c190fb6..0d44e09cb 100644 --- a/packages/app-lib/src/error.rs +++ b/packages/app-lib/src/error.rs @@ -102,6 +102,12 @@ pub enum ErrorKind { #[cfg(feature = "tauri")] #[error("Tauri error: {0}")] TauriError(#[from] tauri::Error), + + #[error("Error interacting with database: {0}")] + Sqlx(#[from] sqlx::Error), + + #[error("Error while applying migrations: {0}")] + SqlxMigrate(#[from] sqlx::migrate::MigrateError), } #[derive(Debug)] diff --git a/packages/app-lib/src/event/emit.rs b/packages/app-lib/src/event/emit.rs index 0375167fd..ebfc25f6b 100644 --- a/packages/app-lib/src/event/emit.rs +++ b/packages/app-lib/src/event/emit.rs @@ -4,7 +4,6 @@ use crate::{ CommandPayload, EventError, LoadingBar, LoadingBarType, ProcessPayloadType, ProfilePayloadType, }, - prelude::ProfilePathId, state::{ProcessType, SafeProcesses}, }; use futures::prelude::*; @@ -297,8 +296,7 @@ pub async fn emit_process( // emit_profile(path, event) #[allow(unused_variables)] pub async fn emit_profile( - uuid: Uuid, - profile_path_id: &ProfilePathId, + profile_path_id: &str, name: &str, event: ProfilePayloadType, ) -> crate::Result<()> { @@ -311,8 +309,7 @@ pub async fn emit_profile( .emit_all( "profile", ProfilePayload { - uuid, - profile_path_id: profile_path_id.clone(), + profile_path_id: profile_path_id.to_string(), path, name: name.to_string(), event, diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs index 0738aea83..7b608fcdb 100644 --- a/packages/app-lib/src/event/mod.rs +++ b/packages/app-lib/src/event/mod.rs @@ -5,7 +5,6 @@ use tokio::sync::OnceCell; use tokio::sync::RwLock; use uuid::Uuid; -use crate::prelude::ProfilePathId; use crate::state::SafeProcesses; pub mod emit; @@ -162,28 +161,28 @@ pub enum LoadingBarType { version: u32, }, PackFileDownload { - profile_path: PathBuf, + profile_path: String, pack_name: String, icon: Option, pack_version: String, }, PackDownload { - profile_path: PathBuf, + profile_path: String, pack_name: String, icon: Option, pack_id: Option, pack_version: Option, }, MinecraftDownload { - profile_path: PathBuf, + profile_path: String, profile_name: String, }, ProfileUpdate { - profile_path: PathBuf, + profile_path: String, profile_name: String, }, ZipExtract { - profile_path: PathBuf, + profile_path: String, profile_name: String, }, ConfigChange { @@ -248,8 +247,7 @@ pub enum ProcessPayloadType { #[derive(Serialize, Clone)] pub struct ProfilePayload { - pub uuid: Uuid, - pub profile_path_id: ProfilePathId, + pub profile_path_id: String, pub path: PathBuf, pub name: String, pub event: ProfilePayloadType, diff --git a/packages/app-lib/src/launcher/download.rs b/packages/app-lib/src/launcher/download.rs index 0dbc4d801..343b87082 100644 --- a/packages/app-lib/src/launcher/download.rs +++ b/packages/app-lib/src/launcher/download.rs @@ -1,7 +1,6 @@ //! Downloader for Minecraft data use crate::launcher::parse_rules; -use crate::state::CredentialsStore; use crate::{ event::{ emit::{emit_loading, loading_try_for_each_concurrent}, @@ -133,7 +132,6 @@ pub async fn download_client( &client_download.url, Some(&client_download.sha1), &st.fetch_semaphore, - &CredentialsStore(None), ) .await?; write(&path, &bytes, &st.io_semaphore).await?; @@ -215,7 +213,7 @@ pub async fn download_assets( async { if !resource_path.exists() || force { let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &CredentialsStore(None))) + .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .await?; write(&resource_path, resource, &st.io_semaphore).await?; tracing::trace!("Fetched asset with hash {hash}"); @@ -229,7 +227,7 @@ pub async fn download_assets( if with_legacy && !resource_path.exists() || force { let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &CredentialsStore(None))) + .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .await?; write(&resource_path, resource, &st.io_semaphore).await?; tracing::trace!("Fetched legacy asset with hash {hash}"); @@ -286,7 +284,7 @@ pub async fn download_libraries( artifact: Some(ref artifact), .. }) => { - let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore, &CredentialsStore(None)) + let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore) .await?; write(&path, &bytes, &st.io_semaphore).await?; tracing::trace!("Fetched library {} to path {:?}", &library.name, &path); @@ -301,7 +299,7 @@ pub async fn download_libraries( &artifact_path ].concat(); - let bytes = fetch(&url, None, &st.fetch_semaphore, &CredentialsStore(None)).await?; + let bytes = fetch(&url, None, &st.fetch_semaphore).await?; write(&path, &bytes, &st.io_semaphore).await?; tracing::trace!("Fetched library {} to path {:?}", &library.name, &path); Ok::<_, crate::Error>(()) @@ -327,7 +325,7 @@ pub async fn download_libraries( ); if let Some(native) = classifiers.get(&parsed_key) { - let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore, &CredentialsStore(None)).await?; + let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?; let reader = std::io::Cursor::new(&data); if let Ok(mut archive) = zip::ZipArchive::new(reader) { match archive.extract(st.directories.version_natives_dir(version).await) { diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index e1ffef964..2f248b002 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -1,9 +1,9 @@ //! Logic for launching Minecraft +use crate::data::ModLoader; use crate::event::emit::{emit_loading, init_or_edit_loading}; use crate::event::{LoadingBarId, LoadingBarType}; use crate::launcher::io::IOError; -use crate::prelude::JavaVersion; -use crate::state::{Credentials, ProfileInstallStage}; +use crate::state::{Credentials, JavaVersion, ProfileInstallStage}; use crate::util::io; use crate::{ process, @@ -13,6 +13,7 @@ use crate::{ use chrono::Utc; use daedalus as d; use daedalus::minecraft::{RuleAction, VersionInfo}; +use daedalus::modded::LoaderVersion; use st::Profile; use std::collections::HashMap; use std::sync::Arc; @@ -114,22 +115,66 @@ pub async fn get_java_version_from_profile( profile: &Profile, version_info: &VersionInfo, ) -> crate::Result> { - if let Some(java) = profile.java.clone().and_then(|x| x.override_version) { - Ok(Some(java)) - } else { - let key = version_info - .java_version - .as_ref() - .map(|it| it.major_version) - .unwrap_or(8); - - let state = State::get().await?; - let settings = state.settings.read().await; + if let Some(java) = profile.java_path.as_ref() { + let java = crate::api::jre::check_jre(std::path::PathBuf::from(java)) + .await + .ok() + .flatten(); - if let Some(java) = settings.java_globals.get(&format!("JAVA_{key}")) { - return Ok(Some(java.clone())); + if let Some(java) = java { + return Ok(Some(java)); } + } + + let key = version_info + .java_version + .as_ref() + .map(|it| it.major_version) + .unwrap_or(8); + + let state = State::get().await?; + + let java_version = JavaVersion::get(key, &state.pool).await?; + + Ok(java_version) +} + +pub async fn get_loader_version_from_profile( + game_version: &str, + loader: ModLoader, + loader_version: Option<&str>, +) -> crate::Result> { + if loader == ModLoader::Vanilla { + return Ok(None); + } + + let version = loader_version.unwrap_or("latest"); + + let filter = |it: &LoaderVersion| match version { + "latest" => true, + "stable" => it.stable, + id => it.id == *id, + }; + let versions = + crate::api::metadata::get_loader_versions(loader.as_meta_str()).await?; + + let loaders = versions.game_versions.into_iter().find(|x| { + x.id.replace(daedalus::modded::DUMMY_REPLACE_STRING, game_version) + == game_version + }); + + if let Some(loaders) = loaders { + let loader_version = loaders.loaders.iter().find(|x| filter(x)).or( + if version == "stable" { + loaders.loaders.first() + } else { + None + }, + ); + + Ok(loader_version.cloned()) + } else { Ok(None) } } @@ -141,59 +186,56 @@ pub async fn install_minecraft( existing_loading_bar: Option, repairing: bool, ) -> crate::Result<()> { - let sync_projects = existing_loading_bar.is_some(); let loading_bar = init_or_edit_loading( existing_loading_bar, LoadingBarType::MinecraftDownload { // If we are downloading minecraft for a profile, provide its name and uuid - profile_name: profile.metadata.name.clone(), - profile_path: profile.get_profile_full_path().await?, + profile_name: profile.name.clone(), + profile_path: profile.path.clone(), }, 100.0, "Downloading Minecraft", ) .await?; - crate::api::profile::edit(&profile.profile_id(), |prof| { + crate::api::profile::edit(&profile.path, |prof| { prof.install_stage = ProfileInstallStage::Installing; async { Ok(()) } }) .await?; - State::sync().await?; - - if sync_projects { - Profile::sync_projects_task(profile.profile_id(), true); - } let state = State::get().await?; + let instance_path = - &io::canonicalize(profile.get_profile_full_path().await?)?; - let metadata = state.metadata.read().await; + crate::api::profile::get_full_path(&profile.path).await?; + let minecraft = crate::api::metadata::get_minecraft_versions().await?; - let version_index = metadata - .minecraft + let version_index = minecraft .versions .iter() - .position(|it| it.id == profile.metadata.game_version) + .position(|it| it.id == profile.game_version) .ok_or(crate::ErrorKind::LauncherError(format!( "Invalid game version: {}", - profile.metadata.game_version + profile.game_version )))?; - let version = &metadata.minecraft.versions[version_index]; + let version = &minecraft.versions[version_index]; let minecraft_updated = version_index - <= metadata - .minecraft + <= minecraft .versions .iter() .position(|x| x.id == "22w16a") .unwrap_or(0); - let version_jar = profile - .metadata - .loader_version - .as_ref() - .map_or(version.id.clone(), |it| { + let loader_version = get_loader_version_from_profile( + &profile.game_version, + profile.loader, + profile.loader_version.as_deref(), + ) + .await?; + + let version_jar = + loader_version.as_ref().map_or(version.id.clone(), |it| { format!("{}-{}", version.id.clone(), it.id.clone()) }); @@ -201,7 +243,7 @@ pub async fn install_minecraft( let mut version_info = download::download_version_info( &state, version, - profile.metadata.loader_version.as_ref(), + loader_version.as_ref(), Some(repairing), Some(&loading_bar), ) @@ -235,13 +277,7 @@ pub async fn install_minecraft( })?; if set_java { - { - let mut settings = state.settings.write().await; - settings - .java_globals - .insert(format!("JAVA_{key}"), java_version.clone()); - } - State::sync().await?; + java_version.upsert(&state.pool).await?; } // Download minecraft (5-90) @@ -274,7 +310,7 @@ pub async fn install_minecraft( client => client_path.to_string_lossy(), server => ""; "MINECRAFT_VERSION": - client => profile.metadata.game_version.clone(), + client => profile.game_version.clone(), server => ""; "ROOT": client => instance_path.to_string_lossy(), @@ -356,13 +392,12 @@ pub async fn install_minecraft( } } - crate::api::profile::edit(&profile.profile_id(), |prof| { + crate::api::profile::edit(&profile.path, |prof| { prof.install_stage = ProfileInstallStage::Installed; async { Ok(()) } }) .await?; - State::sync().await?; emit_loading(&loading_bar, 1.0, Some("Finished installing")).await?; Ok(()) @@ -396,41 +431,43 @@ pub async fn launch_minecraft( } let state = State::get().await?; - let metadata = state.metadata.read().await; - let instance_path = profile.get_profile_full_path().await?; - let instance_path = &io::canonicalize(instance_path)?; + let instance_path = + crate::api::profile::get_full_path(&profile.path).await?; - let version_index = metadata - .minecraft + let minecraft = crate::api::metadata::get_minecraft_versions().await?; + let version_index = minecraft .versions .iter() - .position(|it| it.id == profile.metadata.game_version) + .position(|it| it.id == profile.game_version) .ok_or(crate::ErrorKind::LauncherError(format!( "Invalid game version: {}", - profile.metadata.game_version + profile.game_version )))?; - let version = &metadata.minecraft.versions[version_index]; + let version = &minecraft.versions[version_index]; let minecraft_updated = version_index - <= metadata - .minecraft + <= minecraft .versions .iter() .position(|x| x.id == "22w16a") .unwrap_or(0); - let version_jar = profile - .metadata - .loader_version - .as_ref() - .map_or(version.id.clone(), |it| { + let loader_version = get_loader_version_from_profile( + &profile.game_version, + profile.loader, + profile.loader_version.as_deref(), + ) + .await?; + + let version_jar = + loader_version.as_ref().map_or(version.id.clone(), |it| { format!("{}-{}", version.id.clone(), it.id.clone()) }); let version_info = download::download_version_info( &state, version, - profile.metadata.loader_version.as_ref(), + loader_version.as_ref(), None, None, ) @@ -474,11 +511,11 @@ pub async fn launch_minecraft( // Check if profile has a running profile, and reject running the command if it does // Done late so a quick double call doesn't launch two instances let existing_processes = - process::get_uuids_by_profile_path(profile.profile_id()).await?; + process::get_uuids_by_profile_path(&profile.path).await?; if let Some(uuid) = existing_processes.first() { return Err(crate::ErrorKind::LauncherError(format!( - "Profile {} is already running at UUID: {uuid}", - profile.profile_id() + "Profile {} is already running at path: {uuid}", + profile.path )) .as_error()); } @@ -513,7 +550,7 @@ pub async fn launch_minecraft( credentials, &version.id, &version_info.asset_index.id, - instance_path, + &instance_path, &state.directories.assets_dir().await, &version.type_, *resolution, @@ -561,13 +598,12 @@ pub async fn launch_minecraft( io::write(&options_path, options_string).await?; } - crate::api::profile::edit(&profile.profile_id(), |prof| { - prof.metadata.last_played = Some(Utc::now()); + crate::api::profile::edit(&profile.path, |prof| { + prof.last_played = Some(Utc::now()); async { Ok(()) } }) .await?; - State::sync().await?; let mut censor_strings = HashMap::new(); let username = whoami::username(); @@ -603,8 +639,8 @@ pub async fn launch_minecraft( let window = EventState::get_main_window().await?; if let Some(window) = window { - let settings = state.settings.read().await; - if settings.hide_on_process { + let settings = crate::state::Settings::get(&state.pool).await?; + if settings.hide_on_process_start { window.minimize()?; } } @@ -614,7 +650,7 @@ pub async fn launch_minecraft( // Add game played to discord rich presence let _ = state .discord_rpc - .set_activity(&format!("Playing {}", profile.metadata.name), true) + .set_activity(&format!("Playing {}", profile.name), true) .await; } @@ -624,7 +660,7 @@ pub async fn launch_minecraft( state_children .insert_new_process( Uuid::new_v4(), - profile.profile_id(), + &profile.path, command, post_exit_hook, censor_strings, diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs index 146bafc9f..c2734d9f6 100644 --- a/packages/app-lib/src/lib.rs +++ b/packages/app-lib/src/lib.rs @@ -22,5 +22,4 @@ pub use api::*; pub use error::*; pub use event::{EventState, LoadingBar, LoadingBarType}; pub use logger::start_logger; -pub use state::InnerProjectPathUnix; pub use state::State; diff --git a/packages/app-lib/src/state/cache.rs b/packages/app-lib/src/state/cache.rs new file mode 100644 index 000000000..7dfbed520 --- /dev/null +++ b/packages/app-lib/src/state/cache.rs @@ -0,0 +1,855 @@ +use crate::config::{META_URL, MODRINTH_API_URL, MODRINTH_API_URL_V3}; +use crate::util::fetch::{fetch_json, FetchSemaphore}; +use chrono::{DateTime, Utc}; +use dashmap::DashSet; +use reqwest::Method; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::Display; +use std::hash::Hash; +use std::path::PathBuf; + +// 1 day +const DEFAULT_EXPIRY: i64 = 60 * 60 * 24; +const DEFAULT_ID: &'static str = "0"; + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum CacheValueType { + Project, + Version, + User, + Team, + Organization, + File, + LoaderManifest, + MinecraftManifest, + Categories, + ReportTypes, + Loaders, + GameVersions, + DonationPlatforms, +} + +impl CacheValueType { + pub fn as_str(&self) -> &'static str { + match self { + CacheValueType::Project => "project", + CacheValueType::Version => "version", + CacheValueType::User => "user", + CacheValueType::Team => "team", + CacheValueType::Organization => "organization", + CacheValueType::File => "file", + CacheValueType::LoaderManifest => "loader_manifest", + CacheValueType::MinecraftManifest => "minecraft_manifest", + CacheValueType::Categories => "categories", + CacheValueType::ReportTypes => "report_types", + CacheValueType::Loaders => "loaders", + CacheValueType::GameVersions => "game_versions", + CacheValueType::DonationPlatforms => "donation_platforms", + } + } + + pub fn from_str(val: &str) -> CacheValueType { + match val { + "project" => CacheValueType::Project, + "version" => CacheValueType::Version, + "user" => CacheValueType::User, + "team" => CacheValueType::Team, + "organization" => CacheValueType::Organization, + "file" => CacheValueType::File, + "loader_manifest" => CacheValueType::LoaderManifest, + "minecraft_manifest" => CacheValueType::MinecraftManifest, + "categories" => CacheValueType::Categories, + "report_types" => CacheValueType::ReportTypes, + "loaders" => CacheValueType::Loaders, + "game_versions" => CacheValueType::GameVersions, + "donation_platforms" => CacheValueType::DonationPlatforms, + _ => CacheValueType::Project, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum CacheValue { + Project(Project), + + Version(Version), + + User(User), + + Team(Vec), + + Organization(Organization), + + File(CachedFile), + + LoaderManifest(CachedLoaderManifest), + MinecraftManifest(daedalus::minecraft::VersionManifest), + + Categories(Vec), + ReportTypes(Vec), + Loaders(Vec), + GameVersions(Vec), + DonationPlatforms(Vec), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CachedLoaderManifest { + pub loader: String, + pub manifest: daedalus::modded::Manifest, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CachedFile { + hash: String, + metadata: FileMetadata, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum FileMetadata { + Modrinth { + project_id: String, + version_id: String, + }, + Inferred { + title: Option, + description: Option, + authors: Vec, + version: Option, + icon: Option, + project_type: Option, + }, + Unknown, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Project { + pub id: String, + pub slug: Option, + pub project_type: String, + pub team: String, + pub title: String, + pub description: String, + pub body: String, + + pub published: DateTime, + pub updated: DateTime, + + pub client_side: SideType, + pub server_side: SideType, + + pub downloads: u32, + pub followers: u32, + + pub categories: Vec, + pub additional_categories: Vec, + pub game_versions: Vec, + pub loaders: Vec, + + pub versions: Vec, + + pub icon_url: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum SideType { + Required, + Optional, + Unsupported, + Unknown, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Version { + pub id: String, + pub project_id: String, + pub author_id: String, + + pub featured: bool, + + pub name: String, + pub version_number: String, + pub changelog: String, + pub changelog_url: Option, + + pub date_published: DateTime, + pub downloads: u32, + pub version_type: String, + + pub files: Vec, + pub dependencies: Vec, + pub game_versions: Vec, + pub loaders: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct VersionFile { + pub hashes: HashMap, + pub url: String, + pub filename: String, + pub primary: bool, + pub size: u32, + pub file_type: Option, +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum FileType { + RequiredResourcePack, + OptionalResourcePack, + Unknown, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Dependency { + pub version_id: Option, + pub project_id: Option, + pub file_name: Option, + pub dependency_type: DependencyType, +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum DependencyType { + Required, + Optional, + Incompatible, + Embedded, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TeamMember { + pub team_id: String, + pub user: User, + pub is_owner: bool, + pub role: String, + pub ordering: i64, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct User { + pub id: String, + pub username: String, + pub avatar_url: Option, + pub bio: Option, + pub created: DateTime, + pub role: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Organization { + pub id: String, + pub slug: String, + pub name: String, + pub team_id: String, + pub description: String, + pub icon_url: Option, + pub color: Option, + pub members: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Category { + pub name: String, + pub project_type: String, + pub header: String, + pub icon: PathBuf, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Loader { + pub name: String, + pub icon: PathBuf, + pub supported_project_types: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DonationPlatform { + pub short: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameVersion { + pub version: String, + pub version_type: String, + pub date: String, + pub major: bool, +} + +impl CacheValue { + fn get_type(&self) -> CacheValueType { + match self { + CacheValue::Project(_) => CacheValueType::Project, + CacheValue::Version(_) => CacheValueType::Version, + CacheValue::User(_) => CacheValueType::User, + CacheValue::Team { .. } => CacheValueType::Team, + CacheValue::Organization(_) => CacheValueType::Organization, + CacheValue::File { .. } => CacheValueType::File, + CacheValue::LoaderManifest { .. } => CacheValueType::LoaderManifest, + CacheValue::MinecraftManifest(_) => { + CacheValueType::MinecraftManifest + } + CacheValue::Categories(_) => CacheValueType::Categories, + CacheValue::ReportTypes(_) => CacheValueType::ReportTypes, + CacheValue::Loaders(_) => CacheValueType::Loaders, + CacheValue::GameVersions(_) => CacheValueType::GameVersions, + CacheValue::DonationPlatforms(_) => { + CacheValueType::DonationPlatforms + } + } + } + + fn get_key(&self) -> &str { + match self { + CacheValue::Project(project) => &project.id, + CacheValue::Version(version) => &version.id, + CacheValue::User(user) => &user.id, + CacheValue::Team(members) => members + .iter() + .next() + .map(|x| x.team_id.as_str()) + .unwrap_or(DEFAULT_ID), + CacheValue::Organization(org) => &org.id, + CacheValue::File(file) => &file.hash, + CacheValue::LoaderManifest(loader) => &loader.loader, + // These values can only have one key/val pair, so we specify the same key + CacheValue::MinecraftManifest(_) + | CacheValue::Categories(_) + | CacheValue::ReportTypes(_) + | CacheValue::Loaders(_) + | CacheValue::GameVersions(_) + | CacheValue::DonationPlatforms(_) => DEFAULT_ID, + } + } + + fn get_alias(&self) -> Option<&str> { + match self { + CacheValue::Project(project) => project.slug.as_deref(), + CacheValue::User(user) => Some(&user.username), + CacheValue::Organization(org) => Some(&org.slug), + + CacheValue::MinecraftManifest(_) + | CacheValue::Categories(_) + | CacheValue::ReportTypes(_) + | CacheValue::Loaders(_) + | CacheValue::GameVersions(_) + | CacheValue::DonationPlatforms(_) + | CacheValue::Version(_) + | CacheValue::Team { .. } + | CacheValue::File { .. } + | CacheValue::LoaderManifest { .. } => None, + } + } +} + +#[derive(PartialEq, Eq, Debug)] +pub enum CacheBehaviour { + // Serve expired data, revalidate in background + StaleWhileRevalidate, + // Must revalidate if data is expired + MustRevalidate, + // Ignore cache- always fetch updated data from origin + Bypass, +} + +impl Default for CacheBehaviour { + fn default() -> Self { + Self::StaleWhileRevalidate + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CachedEntry { + id: String, + alias: Option, + #[serde(rename = "data_type")] + type_: CacheValueType, + data: CacheValue, + expires: i64, +} + +macro_rules! impl_cache_methods { + ($(($variant:ident, $type:ty)),*) => { + impl CachedEntry { + $( + paste::paste! { + #[tracing::instrument(skip(exec, fetch_semaphore))] + pub async fn [] <'a, E>( + id: &str, + cache_behaviour: Option, + exec: E, + fetch_semaphore: &FetchSemaphore, + ) -> crate::Result> + where + E: sqlx::Acquire<'a, Database = sqlx::Sqlite>, + { + Ok(Self::[](&[id], cache_behaviour, exec, fetch_semaphore).await?.into_iter().next()) + } + + #[tracing::instrument(skip(exec, fetch_semaphore))] + pub async fn [] <'a, E>( + ids: &[&str], + cache_behaviour: Option, + exec: E, + fetch_semaphore: &FetchSemaphore, + ) -> crate::Result> + where + E: sqlx::Acquire<'a, Database = sqlx::Sqlite>, + { + let entry = + CachedEntry::get_many(CacheValueType::$variant, ids, cache_behaviour, exec, fetch_semaphore).await?; + + Ok(entry.into_iter().filter_map(|x| if let CacheValue::$variant(value) = x.data { + Some(value) + } else { + None + }).collect()) + } + } + )* + } + } +} + +macro_rules! impl_cache_method_singular { + ($(($variant:ident, $type:ty)),*) => { + impl CachedEntry { + $( + paste::paste! { + #[tracing::instrument(skip(exec, fetch_semaphore))] + pub async fn [] <'a, E>( + cache_behaviour: Option, + exec: E, + fetch_semaphore: &FetchSemaphore, + ) -> crate::Result> + where + E: sqlx::Acquire<'a, Database = sqlx::Sqlite>, + { + let entry = + CachedEntry::get(CacheValueType::$variant, DEFAULT_ID, cache_behaviour, exec, fetch_semaphore).await?; + + if let Some(CacheValue::$variant(value)) = entry.map(|x| x.data) { + Ok(Some(value)) + } else { + Ok(None) + } + } + } + )* + } + } +} + +impl_cache_methods!( + (Project, Project), + (Version, Version), + (User, User), + (Team, Vec), + (Organization, Organization), + (File, CachedFile), + (LoaderManifest, CachedLoaderManifest) +); + +impl_cache_method_singular!( + (MinecraftManifest, daedalus::minecraft::VersionManifest), + (Categories, Vec), + (ReportTypes, Vec), + (Loaders, Vec), + (GameVersions, Vec), + (DonationPlatforms, Vec) +); + +impl CachedEntry { + #[tracing::instrument(skip(exec, fetch_semaphore))] + pub async fn get<'a, E>( + type_: CacheValueType, + key: &str, + cache_behaviour: Option, + exec: E, + fetch_semaphore: &FetchSemaphore, + ) -> crate::Result> + where + E: sqlx::Acquire<'a, Database = sqlx::Sqlite>, + { + Ok(Self::get_many( + type_, + &[key], + cache_behaviour, + exec, + fetch_semaphore, + ) + .await? + .into_iter() + .next()) + } + + #[tracing::instrument(skip(conn, fetch_semaphore))] + pub async fn get_many<'a, E>( + type_: CacheValueType, + keys: &[&str], + cache_behaviour: Option, + conn: E, + fetch_semaphore: &FetchSemaphore, + ) -> crate::Result> + where + E: sqlx::Acquire<'a, Database = sqlx::Sqlite>, + { + let cache_behaviour = cache_behaviour.unwrap_or_default(); + + let remaining_keys = DashSet::new(); + for key in keys { + remaining_keys.insert(*key); + } + + let mut return_vals = Vec::new(); + let expired_keys = DashSet::new(); + + let mut exec = conn.acquire().await?; + + if cache_behaviour != CacheBehaviour::Bypass { + let type_ = type_.as_str(); + let keys = serde_json::to_string(&keys)?; + + // unsupported type NULL of column #3 ("data"), so cannot be compile time type checked + // https://github.com/launchbadge/sqlx/issues/1979 + let query = sqlx::query!( + r#" + SELECT id, data_type, json(data) as "data!: serde_json::Value", alias, expires + FROM cache + WHERE data_type = $1 AND ( + id IN (SELECT value FROM json_each($2)) + OR + alias IN (SELECT value FROM json_each($2)) + ) + "#, + type_, + keys + ) + .fetch_all(&mut *exec) + .await?; + + for row in query { + if let Ok(data) = serde_json::from_value(row.data) { + if row.expires <= Utc::now().timestamp() { + if cache_behaviour == CacheBehaviour::MustRevalidate { + continue; + } else { + expired_keys.insert(row.id.clone()); + } + } + + remaining_keys.remove(&*row.id); + if let Some(alias) = row.alias.as_deref() { + remaining_keys.remove(alias); + } + + return_vals.push(Self { + id: row.id, + alias: row.alias, + type_: CacheValueType::from_str(&row.data_type), + data, + expires: row.expires, + }); + } + } + } + + if !remaining_keys.is_empty() { + let mut values = + Self::fetch_many(type_, remaining_keys, fetch_semaphore) + .await?; + + if !values.is_empty() { + Self::upsert_many(&*values, &mut *exec).await?; + + return_vals.append(&mut values); + } + } + + if !expired_keys.is_empty() + && cache_behaviour == CacheBehaviour::StaleWhileRevalidate + { + let _ = tokio::task::spawn(async move { + // TODO: if possible- find a way to do this without invoking state get + let state = crate::state::State::get().await?; + + let values = Self::fetch_many( + type_, + expired_keys, + &state.fetch_semaphore, + ) + .await?; + + if !values.is_empty() { + Self::upsert_many(&*values, &state.pool).await?; + } + + Ok::<(), crate::Error>(()) + }); + } + + Ok(return_vals) + } + + async fn fetch_many( + type_: CacheValueType, + keys: DashSet, + fetch_semaphore: &FetchSemaphore, + ) -> crate::Result> { + macro_rules! fetch_original_values { + ($type:ident, $api_url:expr, $url_suffix:expr, $cache_variant:path) => {{ + fetch_json::>( + Method::GET, + &*format!( + "{}{}?ids={}", + $api_url, + $url_suffix, + serde_json::to_string(&keys)? + ), + None, + None, + &fetch_semaphore, + ) + .await? + .into_iter() + .map(|x| { + let data = $cache_variant(x); + + Self { + id: data.get_key().to_string(), + alias: data.get_alias().map(|x| x.to_string()), + type_: CacheValueType::$type, + data, + expires: Utc::now().timestamp() + DEFAULT_EXPIRY, + } + }) + .collect() + }}; + } + + macro_rules! fetch_original_value { + ($type:ident, $api_url:expr, $url_suffix:expr, $cache_variant:path) => {{ + vec![Self { + id: DEFAULT_ID.to_string(), + alias: None, + type_: CacheValueType::$type, + data: $cache_variant( + fetch_json( + Method::GET, + &*format!("{}{}", $api_url, $url_suffix), + None, + None, + &fetch_semaphore, + ) + .await?, + ), + expires: Utc::now().timestamp() + DEFAULT_EXPIRY, + }] + }}; + } + + Ok(match type_ { + CacheValueType::Project => { + fetch_original_values!( + Project, + MODRINTH_API_URL, + "projects", + CacheValue::Project + ) + } + CacheValueType::Version => { + fetch_original_values!( + Version, + MODRINTH_API_URL, + "versions", + CacheValue::Version + ) + } + CacheValueType::User => { + fetch_original_values!( + User, + MODRINTH_API_URL, + "users", + CacheValue::User + ) + } + CacheValueType::Team => { + fetch_original_values!( + Team, + MODRINTH_API_URL, + "teams", + CacheValue::Team + ) + } + CacheValueType::Organization => { + fetch_original_values!( + Organization, + MODRINTH_API_URL_V3, + "organizations", + CacheValue::Organization + ) + } + CacheValueType::File => fetch_json::>( + Method::POST, + &*format!("{}version_files", MODRINTH_API_URL), + None, + Some(serde_json::json!({ + "algorithm": "sha1", + "hashes": keys, + })), + &fetch_semaphore, + ) + .await? + .into_iter() + .map(|(hash, version)| Self { + id: hash.clone(), + alias: None, + type_: CacheValueType::File, + data: CacheValue::File(CachedFile { + hash, + metadata: FileMetadata::Modrinth { + project_id: version.project_id, + version_id: version.id, + }, + }), + expires: Utc::now().timestamp() + DEFAULT_EXPIRY, + }) + .collect(), + CacheValueType::LoaderManifest => { + let fetch_urls = keys + .iter() + .map(|x| { + ( + x.key().to_string(), + format!("{META_URL}{}/v0/manifest.json", x.key()), + ) + }) + .collect::>(); + + futures::future::try_join_all(fetch_urls.iter().map( + |(_, url)| { + fetch_json( + Method::GET, + url, + None, + None, + fetch_semaphore, + ) + }, + )) + .await? + .into_iter() + .enumerate() + .map(|(index, metadata)| Self { + id: fetch_urls[index].0.to_string(), + alias: None, + type_: CacheValueType::LoaderManifest, + data: CacheValue::LoaderManifest(CachedLoaderManifest { + loader: fetch_urls[index].0.to_string(), + manifest: metadata, + }), + expires: Utc::now().timestamp() + DEFAULT_EXPIRY, + }) + .collect() + } + CacheValueType::MinecraftManifest => { + fetch_original_value!( + MinecraftManifest, + META_URL, + format!( + "minecraft/v{}/manifest.json", + daedalus::minecraft::CURRENT_FORMAT_VERSION + ), + CacheValue::MinecraftManifest + ) + } + CacheValueType::Categories => { + fetch_original_value!( + Categories, + MODRINTH_API_URL, + "tag/category", + CacheValue::Categories + ) + } + CacheValueType::ReportTypes => { + fetch_original_value!( + ReportTypes, + MODRINTH_API_URL, + "tag/report_type", + CacheValue::ReportTypes + ) + } + CacheValueType::Loaders => { + fetch_original_value!( + Loaders, + MODRINTH_API_URL, + "tag/loader", + CacheValue::Loaders + ) + } + CacheValueType::GameVersions => { + fetch_original_value!( + GameVersions, + MODRINTH_API_URL, + "tag/game_version", + CacheValue::GameVersions + ) + } + CacheValueType::DonationPlatforms => { + fetch_original_value!( + DonationPlatforms, + MODRINTH_API_URL, + "tag/donation_platform", + CacheValue::DonationPlatforms + ) + } + }) + } + + /// Update/sets a value in the cache to the given value. Avoid using if possible: + /// stick to `Self::get` and `Self::get_many`. + pub(crate) async fn upsert( + self, + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result<()> { + Self::upsert_many(&[self], exec).await + } + + /// Update/sets values in the cache to the given values. Avoid using if possible: + /// stick to `Self::get` and `Self::get_many`. + pub(crate) async fn upsert_many( + items: &[Self], + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result<()> { + let items = serde_json::to_string(items)?; + + sqlx::query!( + " + INSERT INTO cache (id, data_type, alias, data, expires) + SELECT + json_extract(value, '$.id') AS id, + json_extract(value, '$.data_type') AS data_type, + json_extract(value, '$.alias') AS alias, + json_extract(value, '$.data') AS data, + json_extract(value, '$.expires') AS expires + FROM + json_each($1) + WHERE TRUE + ON CONFLICT (id, data_type) DO UPDATE SET + alias = excluded.alias, + data = excluded.data, + expires = excluded.expires + ", + items, + ) + .execute(exec) + .await?; + + Ok(()) + } +} diff --git a/packages/app-lib/src/state/children.rs b/packages/app-lib/src/state/children.rs index f272276d4..bc7c62c6e 100644 --- a/packages/app-lib/src/state/children.rs +++ b/packages/app-lib/src/state/children.rs @@ -1,4 +1,4 @@ -use super::{Profile, ProfilePathId}; +use super::Profile; use chrono::{DateTime, Utc}; use serde::Deserialize; use serde::Serialize; @@ -37,7 +37,7 @@ pub struct ProcessCache { pub start_time: u64, pub name: String, pub exe: String, - pub profile_relative_path: ProfilePathId, + pub profile_relative_path: String, pub post_command: Option, } impl ChildType { @@ -94,7 +94,7 @@ impl ChildType { pub async fn cache_process( &self, uuid: uuid::Uuid, - profile_path_id: ProfilePathId, + profile_path_id: String, post_command: Option, ) -> crate::Result<()> { let pid = match self { @@ -198,7 +198,7 @@ impl ChildType { #[derive(Debug)] pub struct MinecraftChild { pub uuid: Uuid, - pub profile_relative_path: ProfilePathId, + pub profile_relative_path: String, pub manager: Option>>, // None when future has completed and been handled pub current_child: Arc>, pub last_updated_playtime: DateTime, // The last time we updated the playtime for the associated profile @@ -272,7 +272,7 @@ impl Children { pub async fn insert_new_process( &mut self, uuid: Uuid, - profile_relative_path: ProfilePathId, + profile_relative_path: &str, mut mc_command: Command, post_command: Option, // Command to run after minecraft. censor_strings: HashMap, @@ -293,7 +293,7 @@ impl Children { child .cache_process( uuid, - profile_relative_path.clone(), + profile_relative_path.to_string(), post_command.clone(), ) .await?; @@ -303,7 +303,7 @@ impl Children { post_command, pid, current_child.clone(), - profile_relative_path.clone(), + profile_relative_path.to_string(), ))); emit_process( @@ -319,7 +319,7 @@ impl Children { // Create MinecraftChild let mchild = MinecraftChild { uuid, - profile_relative_path, + profile_relative_path: profile_relative_path.to_string(), current_child, manager, last_updated_playtime, @@ -438,7 +438,7 @@ impl Children { post_command: Option, mut current_pid: u32, current_child: Arc>, - associated_profile: ProfilePathId, + associated_profile: String, ) -> crate::Result { let current_child = current_child.clone(); @@ -459,7 +459,7 @@ impl Children { .num_seconds(); if diff >= 60 { if let Err(e) = profile::edit(&associated_profile, |prof| { - prof.metadata.recent_time_played += diff as u64; + prof.recent_time_played += diff as u64; async { Ok(()) } }) .await @@ -479,7 +479,7 @@ impl Children { .signed_duration_since(last_updated_playtime) .num_seconds(); if let Err(e) = profile::edit(&associated_profile, |prof| { - prof.metadata.recent_time_played += diff as u64; + prof.recent_time_played += diff as u64; async { Ok(()) } }) .await @@ -547,9 +547,10 @@ impl Children { let mut cmd = hook.split(' '); if let Some(command) = cmd.next() { let mut command = Command::new(command); - command - .args(&cmd.collect::>()) - .current_dir(associated_profile.get_full_path().await?); + command.args(&cmd.collect::>()).current_dir( + crate::api::profile::get_full_path(&associated_profile) + .await?, + ); Some(command) } else { None @@ -650,7 +651,7 @@ impl Children { // Gets all PID keys of running children with a given profile path pub async fn running_keys_with_profile( &self, - profile_path: ProfilePathId, + profile_path: &str, ) -> crate::Result> { let running_keys = self.running_keys().await?; let mut keys = Vec::new(); @@ -667,9 +668,7 @@ impl Children { } // Gets all profiles of running children - pub async fn running_profile_paths( - &self, - ) -> crate::Result> { + pub async fn running_profile_paths(&self) -> crate::Result> { let mut profiles = Vec::new(); for key in self.keys() { if let Some(child) = self.get(key) { @@ -708,7 +707,6 @@ impl Children { { if let Some(prof) = crate::api::profile::get( &child.profile_relative_path.clone(), - None, ) .await? { diff --git a/packages/app-lib/src/state/db/mod.rs b/packages/app-lib/src/state/db/mod.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index a981abcdb..4082f26d9 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -4,8 +4,6 @@ use std::path::PathBuf; use tokio::sync::RwLock; -use super::{ProfilePathId, Settings}; - pub const SETTINGS_FILE_NAME: &str = "settings.json"; pub const CACHES_FOLDER_NAME: &str = "caches"; pub const LAUNCHER_LOGS_FOLDER_NAME: &str = "launcher_logs"; @@ -24,22 +22,22 @@ impl DirectoryInfo { // init() is not needed for this function pub fn get_initial_settings_dir() -> Option { Self::env_path("THESEUS_CONFIG_DIR") - .or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus"))) + .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) } #[inline] - pub fn get_initial_settings_file() -> crate::Result { + pub fn get_database_file() -> crate::Result { let settings_dir = Self::get_initial_settings_dir().ok_or( crate::ErrorKind::FSError( "Could not find valid config dir".to_string(), ), )?; - Ok(settings_dir.join("settings.json")) + Ok(settings_dir.join("app.db")) } /// Get all paths needed for Theseus to operate properly #[tracing::instrument] - pub fn init(settings: &Settings) -> crate::Result { + pub fn init() -> crate::Result { // Working directory let working_dir = std::env::current_dir().map_err(|err| { crate::ErrorKind::FSError(format!( @@ -59,17 +57,9 @@ impl DirectoryInfo { )) })?; - // config directory (for instances, etc.) - // by default this is the same as the settings directory - let config_dir = settings.loaded_config_dir.clone().ok_or( - crate::ErrorKind::FSError( - "Could not find valid config dir".to_string(), - ), - )?; - Ok(Self { - settings_dir, - config_dir: RwLock::new(config_dir), + settings_dir: settings_dir.clone(), + config_dir: RwLock::new(settings_dir), working_dir, }) } @@ -161,17 +151,23 @@ impl DirectoryInfo { /// Gets the logs dir for a given profile #[inline] pub async fn profile_logs_dir( - profile_id: &ProfilePathId, + &self, + profile_path: &str, ) -> crate::Result { - Ok(profile_id.get_full_path().await?.join("logs")) + Ok(self.profiles_dir().await.join(profile_path).join("logs")) } /// Gets the crash reports dir for a given profile #[inline] pub async fn crash_reports_dir( - profile_id: &ProfilePathId, + &self, + profile_path: &str, ) -> crate::Result { - Ok(profile_id.get_full_path().await?.join("crash-reports")) + Ok(self + .profiles_dir() + .await + .join(profile_path) + .join("crash-reports")) } #[inline] diff --git a/packages/app-lib/src/state/discord.rs b/packages/app-lib/src/state/discord.rs index 9f17c4bfb..413c6fe36 100644 --- a/packages/app-lib/src/state/discord.rs +++ b/packages/app-lib/src/state/discord.rs @@ -82,8 +82,8 @@ impl DiscordGuard { // Check if discord is disabled, and if so, clear the activity instead let state = State::get().await?; - let settings = state.settings.read().await; - if settings.disable_discord_rpc { + let settings = crate::state::Settings::get(&state.pool).await?; + if !settings.discord_rpc { Ok(self.clear_activity(true).await?) } else { Ok(self.force_set_activity(msg, reconnect_if_fail).await?) @@ -184,17 +184,13 @@ impl DiscordGuard { &self, reconnect_if_fail: bool, ) -> crate::Result<()> { - let state: Arc> = - State::get().await?; + let state = State::get().await?; - { - let settings = state.settings.read().await; - if settings.disable_discord_rpc { - println!("Discord is disabled, clearing activity"); - return self.clear_activity(true).await; - } + let settings = crate::state::Settings::get(&state.pool).await?; + if !settings.discord_rpc { + println!("Discord is disabled, clearing activity"); + return self.clear_activity(true).await; } - if let Some(existing_child) = state .children .read() diff --git a/packages/app-lib/src/state/java_globals.rs b/packages/app-lib/src/state/java_globals.rs index 10b8071e2..e1fd2a079 100644 --- a/packages/app-lib/src/state/java_globals.rs +++ b/packages/app-lib/src/state/java_globals.rs @@ -1,66 +1,94 @@ +use dashmap::DashMap; +use futures::TryStreamExt; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; -use crate::prelude::JavaVersion; -use crate::util::jre; +#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)] +pub struct JavaVersion { + pub major_version: u32, + pub version: String, + pub architecture: String, + pub path: String, +} -// All stored Java versions, chosen by the user -// A wrapper over a Hashmap connecting key -> java version -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct JavaGlobals(HashMap); +impl JavaVersion { + pub async fn get( + major_version: u32, + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result> { + let version = major_version as i32; -impl JavaGlobals { - pub fn new() -> JavaGlobals { - JavaGlobals(HashMap::new()) - } + let res = sqlx::query!( + " + SELECT + full_version, architecture, path + FROM java_versions + WHERE major_version = $1 + ", + version + ) + .fetch_optional(exec) + .await?; - pub fn insert(&mut self, key: String, java: JavaVersion) { - self.0.insert(key, java); + Ok(res.map(|x| JavaVersion { + major_version, + version: x.full_version, + architecture: x.architecture, + path: x.path, + })) } - pub fn remove(&mut self, key: &String) { - self.0.remove(key); - } + pub async fn get_all( + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result> { + let res = sqlx::query!( + " + SELECT + major_version, full_version, architecture, path + FROM java_versions + " + ) + .fetch(exec) + .try_fold(DashMap::new(), |acc, x| { + acc.insert( + x.major_version as u32, + JavaVersion { + major_version: x.major_version as u32, + version: x.full_version, + architecture: x.architecture, + path: x.path, + }, + ); - pub fn get(&self, key: &String) -> Option<&JavaVersion> { - self.0.get(key) - } + async move { Ok(acc) } + }) + .await?; - pub fn get_mut(&mut self, key: &String) -> Option<&mut JavaVersion> { - self.0.get_mut(key) + Ok(res) } - pub fn count(&self) -> usize { - self.0.len() - } + pub async fn upsert( + &self, + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result<()> { + let major_version = self.major_version as i32; - pub fn keys(&self) -> Vec { - self.0.keys().cloned().collect() - } - - // Validates that every path here is a valid Java version and that the version matches the version stored here - // If false, when checked, the user should be prompted to reselect the Java version - pub async fn is_all_valid(&self) -> bool { - for (_, java) in self.0.iter() { - let jre = jre::check_java_at_filepath( - PathBuf::from(&java.path).as_path(), - ) - .await; - if let Some(jre) = jre { - if jre.version != java.version { - return false; - } - } else { - return false; - } - } - true - } -} + sqlx::query!( + " + INSERT INTO java_versions (major_version, full_version, architecture, path) + VALUES ($1, $2, $3, $4) + ON CONFLICT (major_version) DO UPDATE SET + full_version = $2, + architecture = $3, + path = $4 + ", + major_version, + self.version, + self.architecture, + self.path, + ) + .execute(exec) + .await?; -impl Default for JavaGlobals { - fn default() -> Self { - Self::new() + Ok(()) } } diff --git a/packages/app-lib/src/state/metadata.rs b/packages/app-lib/src/state/metadata.rs deleted file mode 100644 index e50bb2f2e..000000000 --- a/packages/app-lib/src/state/metadata.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Theseus metadata -use crate::data::DirectoryInfo; -use crate::util::fetch::{read_json, write, IoSemaphore}; -use crate::State; -use daedalus::{ - minecraft::{fetch_version_manifest, VersionManifest as MinecraftManifest}, - modded::{ - fetch_manifest as fetch_loader_manifest, Manifest as LoaderManifest, - }, -}; -use serde::{Deserialize, Serialize}; - -const METADATA_URL: &str = "https://meta.modrinth.com"; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Metadata { - pub minecraft: MinecraftManifest, - pub forge: LoaderManifest, - pub fabric: LoaderManifest, - pub quilt: LoaderManifest, - pub neoforge: LoaderManifest, -} - -impl Metadata { - fn get_manifest(name: &str) -> String { - format!("{METADATA_URL}/{name}/v0/manifest.json") - } - - pub async fn fetch() -> crate::Result { - let (minecraft, forge, fabric, quilt, neoforge) = tokio::try_join! { - async { - let url = Self::get_manifest("minecraft"); - fetch_version_manifest(Some(&url)).await - }, - async { - let url = Self::get_manifest("forge"); - fetch_loader_manifest(&url).await - }, - async { - let url = Self::get_manifest("fabric"); - fetch_loader_manifest(&url).await - }, - async { - let url = Self::get_manifest("quilt"); - fetch_loader_manifest(&url).await - }, - async { - let url = Self::get_manifest("neo"); - fetch_loader_manifest(&url).await - } - }?; - - Ok(Self { - minecraft, - forge, - fabric, - quilt, - neoforge, - }) - } - - // Attempt to fetch metadata and store in sled DB - #[tracing::instrument(skip(io_semaphore))] - #[theseus_macros::debug_pin] - pub async fn init( - dirs: &DirectoryInfo, - fetch_online: bool, - io_semaphore: &IoSemaphore, - ) -> crate::Result { - let mut metadata = None; - let metadata_path = dirs.caches_meta_dir().await.join("metadata.json"); - let metadata_backup_path = - dirs.caches_meta_dir().await.join("metadata.json.bak"); - - if let Ok(metadata_json) = - read_json::(&metadata_path, io_semaphore).await - { - metadata = Some(metadata_json); - } else if fetch_online { - let res = async { - let metadata_fetch = Self::fetch().await?; - - write( - &metadata_path, - &serde_json::to_vec(&metadata_fetch).unwrap_or_default(), - io_semaphore, - ) - .await?; - - write( - &metadata_backup_path, - &serde_json::to_vec(&metadata_fetch).unwrap_or_default(), - io_semaphore, - ) - .await?; - - metadata = Some(metadata_fetch); - Ok::<(), crate::Error>(()) - } - .await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!("Unable to fetch launcher metadata: {err}") - } - } - } else if let Ok(metadata_json) = - read_json::(&metadata_backup_path, io_semaphore).await - { - metadata = Some(metadata_json); - std::fs::copy(&metadata_backup_path, &metadata_path).map_err( - |err| { - crate::ErrorKind::FSError(format!( - "Error restoring metadata backup: {err}" - )) - .as_error() - }, - )?; - } - - if let Some(meta) = metadata { - Ok(meta) - } else { - Err( - crate::ErrorKind::NoValueFor(String::from("launcher metadata")) - .as_error(), - ) - } - } - - pub async fn update() { - let res = async { - let metadata_fetch = Metadata::fetch().await?; - let state = State::get().await?; - - let metadata_path = state - .directories - .caches_meta_dir() - .await - .join("metadata.json"); - let metadata_backup_path = state - .directories - .caches_meta_dir() - .await - .join("metadata.json.bak"); - - if metadata_path.exists() { - std::fs::copy(&metadata_path, &metadata_backup_path)?; - } - - write( - &metadata_path, - &serde_json::to_vec(&metadata_fetch)?, - &state.io_semaphore, - ) - .await?; - - let mut old_metadata = state.metadata.write().await; - *old_metadata = metadata_fetch; - - Ok::<(), crate::Error>(()) - } - .await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!("Unable to update launcher metadata: {err}") - } - }; - } -} diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index fb7928a2c..eff699cb3 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -1,42 +1,31 @@ //! Theseus state management system use crate::event::emit::{emit_loading, emit_offline, init_loading_unsafe}; -use std::path::PathBuf; use crate::event::LoadingBarType; use crate::loading_join; use crate::util::fetch::{self, FetchSemaphore, IoSemaphore}; -use notify::RecommendedWatcher; -use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; use std::sync::Arc; use std::time::Duration; -use tokio::join; use tokio::sync::{OnceCell, RwLock, Semaphore}; -use futures::{channel::mpsc::channel, SinkExt, StreamExt}; +use sqlx::migrate::MigrateDatabase; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::{Connection, Sqlite, SqliteConnection, SqlitePool}; // Submodules mod dirs; pub use self::dirs::*; -mod metadata; -pub use self::metadata::*; - mod profiles; pub use self::profiles::*; mod settings; pub use self::settings::*; -mod projects; -pub use self::projects::*; - mod children; pub use self::children::*; -mod tags; -pub use self::tags::*; - mod java_globals; pub use self::java_globals::*; @@ -49,12 +38,25 @@ pub use self::discord::*; mod minecraft_auth; pub use self::minecraft_auth::*; +mod cache; +pub use self::cache::*; + mod mr_auth; + pub use self::mr_auth::*; +// TODO: Add directory changing support again +// TODO: Add converter for legacy stuff +// TODO: UI: Change so new settings model works +// TODO: UI: Change so new java version API works +// TODO: Profile projects read from disk & cache +// TODO: re-add instance groups +// TODO: profile management methods +// TODO: add back file watcher + // Global state // RwLock on state only has concurrent reads, except for config dir change which takes control of the State -static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); +static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); pub struct State { /// Whether or not the launcher is currently operating in 'offline mode' pub offline: RwLock, @@ -64,23 +66,11 @@ pub struct State { /// Semaphore used to limit concurrent network requests and avoid errors pub fetch_semaphore: FetchSemaphore, - /// Stored maximum number of sempahores of current fetch_semaphore - pub fetch_semaphore_max: RwLock, /// Semaphore used to limit concurrent I/O and avoid errors pub io_semaphore: IoSemaphore, - /// Stored maximum number of sempahores of current io_semaphore - pub io_semaphore_max: RwLock, - /// Launcher metadata - pub metadata: RwLock, - /// Launcher configuration - pub settings: RwLock, /// Reference to minecraft process children pub children: RwLock, - /// Launcher profile metadata - pub(crate) profiles: RwLock, - /// Launcher tags - pub(crate) tags: RwLock, /// Launcher processes that should be safely exited on shutdown pub(crate) safety_processes: RwLock, /// Launcher user account info @@ -93,33 +83,25 @@ pub struct State { /// Discord RPC pub discord_rpc: DiscordGuard, - /// File watcher debouncer - pub(crate) file_watcher: RwLock>, + pub(crate) pool: SqlitePool, } impl State { - /// Get the current launcher state, initializing it if needed - pub async fn get( - ) -> crate::Result>> { - Ok(Arc::new( - LAUNCHER_STATE - .get_or_try_init(Self::initialize_state) - .await? - .read() - .await, - )) + pub async fn init() -> crate::Result<()> { + LAUNCHER_STATE + .get_or_try_init(Self::initialize_state) + .await?; + + Ok(()) } /// Get the current launcher state, initializing it if needed - /// Takes writing control of the state, blocking all other uses of it - /// Only used for state change such as changing the config directory - pub async fn get_write( - ) -> crate::Result> { - Ok(LAUNCHER_STATE - .get_or_try_init(Self::initialize_state) - .await? - .write() - .await) + pub async fn get() -> crate::Result> { + if !LAUNCHER_STATE.initialized() { + while !LAUNCHER_STATE.initialized() {} + } + + Ok(Arc::clone(LAUNCHER_STATE.get().unwrap())) } pub fn initialized() -> bool { @@ -128,7 +110,7 @@ impl State { #[tracing::instrument] #[theseus_macros::debug_pin] - async fn initialize_state() -> crate::Result> { + async fn initialize_state() -> crate::Result> { let loading_bar = init_loading_unsafe( LoadingBarType::StateInit, 100.0, @@ -136,16 +118,30 @@ impl State { ) .await?; - // Settings - let settings = - Settings::init(&DirectoryInfo::get_initial_settings_file()?) - .await?; + let directories = DirectoryInfo::init()?; - let directories = DirectoryInfo::init(&settings)?; + // TODO: move to own file + // db code + let uri = + format!("sqlite:{}", DirectoryInfo::get_database_file()?.display()); - emit_loading(&loading_bar, 10.0, None).await?; + if !Sqlite::database_exists(&uri).await? { + Sqlite::create_database(&uri).await?; + } + + let mut conn: SqliteConnection = + SqliteConnection::connect(&uri).await?; + sqlx::migrate!().run(&mut conn).await?; + + let pool = SqlitePoolOptions::new() + .max_connections(100) + .connect(&uri) + .await?; + // end db code - let mut file_watcher = init_watcher().await?; + let settings = Settings::get(&pool).await?; + + emit_loading(&loading_bar, 10.0, None).await?; let fetch_semaphore = FetchSemaphore(RwLock::new(Semaphore::new( settings.max_concurrent_downloads, @@ -157,24 +153,11 @@ impl State { let is_offline = !fetch::check_internet(3).await; - let metadata_fut = - Metadata::init(&directories, !is_offline, &io_semaphore); - let profiles_fut = Profiles::init(&directories, &mut file_watcher); - let tags_fut = Tags::init( - &directories, - !is_offline, - &io_semaphore, - &fetch_semaphore, - &CredentialsStore(None), - ); let users_fut = MinecraftAuthStore::init(&directories, &io_semaphore); let creds_fut = CredentialsStore::init(&directories, &io_semaphore); // Launcher data - let (metadata, profiles, tags, users, creds) = loading_join! { + let (users, creds) = loading_join! { Some(&loading_bar), 70.0, Some("Loading metadata"); - metadata_fut, - profiles_fut, - tags_fut, users_fut, creds_fut, }?; @@ -182,7 +165,7 @@ impl State { let safety_processes = SafeProcesses::new(); let discord_rpc = DiscordGuard::init(is_offline).await?; - if !settings.disable_discord_rpc && !is_offline { + if settings.discord_rpc && !is_offline { // Add default Idling to discord rich presence // Force add to avoid recursion let _ = discord_rpc.force_set_activity("Idling...", true).await; @@ -195,28 +178,18 @@ impl State { emit_loading(&loading_bar, 10.0, None).await?; - Ok::, crate::Error>(RwLock::new(Self { + Ok(Arc::new(Self { offline: RwLock::new(is_offline), directories, fetch_semaphore, - fetch_semaphore_max: RwLock::new( - settings.max_concurrent_downloads as u32, - ), io_semaphore, - io_semaphore_max: RwLock::new( - settings.max_concurrent_writes as u32, - ), - metadata: RwLock::new(metadata), - settings: RwLock::new(settings), - profiles: RwLock::new(profiles), users: RwLock::new(users), children: RwLock::new(children), credentials: RwLock::new(creds), - tags: RwLock::new(tags), discord_rpc, safety_processes: RwLock::new(safety_processes), - file_watcher: RwLock::new(file_watcher), modrinth_auth_flow: RwLock::new(None), + pool, })) } @@ -240,84 +213,14 @@ impl State { tokio::task::spawn(async { if let Ok(state) = crate::State::get().await { if !*state.offline.read().await { - let res1 = Profiles::update_modrinth_versions(); - let res2 = Tags::update(); - let res3 = Metadata::update(); - let res4 = Profiles::update_projects(); let res6 = CredentialsStore::update_creds(); - let _ = join!(res1, res2, res3, res4, res6); + let _ = res6.await; } } }); } - #[tracing::instrument] - #[theseus_macros::debug_pin] - /// Synchronize in-memory state with persistent state - pub async fn sync() -> crate::Result<()> { - let state = Self::get().await?; - let sync_settings = async { - let state = Arc::clone(&state); - - tokio::spawn(async move { - let reader = state.settings.read().await; - reader.sync(&state.directories.settings_file()).await?; - Ok::<_, crate::Error>(()) - }) - .await? - }; - - let sync_profiles = async { - let state = Arc::clone(&state); - - tokio::spawn(async move { - let profiles = state.profiles.read().await; - - profiles.sync().await?; - Ok::<_, crate::Error>(()) - }) - .await? - }; - - tokio::try_join!(sync_settings, sync_profiles)?; - Ok(()) - } - - /// Reset IO semaphore to default values - /// This will block until all uses of the semaphore are complete, so it should only be called - /// when we are not in the middle of downloading something (ie: changing the settings!) - pub async fn reset_io_semaphore(&self) { - let settings = self.settings.read().await; - let mut io_semaphore = self.io_semaphore.0.write().await; - let mut total_permits = self.io_semaphore_max.write().await; - - // Wait to get all permits back - let _ = io_semaphore.acquire_many(*total_permits).await; - - // Reset the semaphore - io_semaphore.close(); - *total_permits = settings.max_concurrent_writes as u32; - *io_semaphore = Semaphore::new(settings.max_concurrent_writes); - } - - /// Reset IO semaphore to default values - /// This will block until all uses of the semaphore are complete, so it should only be called - /// when we are not in the middle of downloading something (ie: changing the settings!) - pub async fn reset_fetch_semaphore(&self) { - let settings = self.settings.read().await; - let mut io_semaphore = self.fetch_semaphore.0.write().await; - let mut total_permits = self.fetch_semaphore_max.write().await; - - // Wait to get all permits back - let _ = io_semaphore.acquire_many(*total_permits).await; - - // Reset the semaphore - io_semaphore.close(); - *total_permits = settings.max_concurrent_downloads as u32; - *io_semaphore = Semaphore::new(settings.max_concurrent_downloads); - } - /// Refreshes whether or not the launcher should be offline, by whether or not there is an internet connection pub async fn refresh_offline(&self) -> crate::Result<()> { let is_online = fetch::check_internet(3).await; @@ -333,78 +236,3 @@ impl State { Ok(()) } } - -pub async fn init_watcher() -> crate::Result> { - let (mut tx, mut rx) = channel(1); - - let file_watcher = new_debouncer( - Duration::from_secs_f32(2.0), - move |res: DebounceEventResult| { - futures::executor::block_on(async { - tx.send(res).await.unwrap(); - }) - }, - )?; - tokio::task::spawn(async move { - let span = tracing::span!(tracing::Level::INFO, "init_watcher"); - tracing::info!(parent: &span, "Initting watcher"); - while let Some(res) = rx.next().await { - let _span = span.enter(); - match res { - Ok(mut events) => { - let mut visited_paths = Vec::new(); - // sort events by e.path - events.sort_by(|a, b| a.path.cmp(&b.path)); - events.iter().for_each(|e| { - let mut new_path = PathBuf::new(); - let mut components_iterator = e.path.components(); - let mut found = false; - for component in components_iterator.by_ref() { - new_path.push(component); - if found { - break; - } - if component.as_os_str() == "profiles" { - found = true; - } - } - // if any remain, it's a subfile of the profile folder and not the profile folder itself - let subfile = components_iterator.next().is_some(); - - // At this point, new_path is the path to the profile, and subfile is whether it's a subfile of the profile or not - let profile_path_id = - ProfilePathId::new(PathBuf::from( - new_path.file_name().unwrap_or_default(), - )); - - if e.path - .components() - .any(|x| x.as_os_str() == "crash-reports") - && e.path - .extension() - .map(|x| x == "txt") - .unwrap_or(false) - { - Profile::crash_task(profile_path_id); - } else if !visited_paths.contains(&new_path) { - if subfile { - Profile::sync_projects_task( - profile_path_id, - false, - ); - visited_paths.push(new_path); - } else { - Profiles::sync_available_profiles_task( - profile_path_id, - ); - } - } - }); - } - Err(error) => tracing::warn!("Unable to watch file: {error}"), - } - } - }); - - Ok(file_watcher) -} diff --git a/packages/app-lib/src/state/mr_auth.rs b/packages/app-lib/src/state/mr_auth.rs index a3949b041..e30ab1eae 100644 --- a/packages/app-lib/src/state/mr_auth.rs +++ b/packages/app-lib/src/state/mr_auth.rs @@ -13,22 +13,11 @@ use std::collections::HashMap; const AUTH_JSON: &str = "auth.json"; -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ModrinthUser { - pub id: String, - pub username: String, - pub name: Option, - pub avatar_url: Option, - pub bio: Option, - pub created: DateTime, - pub role: String, -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ModrinthCredentials { pub session: String, pub expires_at: DateTime, - pub user: ModrinthUser, + pub user: crate::state::cache::User, } #[derive(Serialize)] @@ -232,7 +221,6 @@ pub async fn login_password( None, None, semaphore, - &CredentialsStore(None), ) .await?; let value = serde_json::from_slice::>(&resp)?; @@ -283,7 +271,6 @@ pub async fn login_2fa( None, None, semaphore, - &CredentialsStore(None), ) .await?; @@ -314,7 +301,6 @@ pub async fn create_account( None, None, semaphore, - &CredentialsStore(None), ) .await?; let response = serde_json::from_slice::>(&resp)?; @@ -336,7 +322,6 @@ pub async fn refresh_credentials( Some(("Authorization", token)), None, semaphore, - &CredentialsStore(None), ) .await .ok() @@ -358,7 +343,7 @@ pub async fn refresh_credentials( async fn fetch_info( token: &str, semaphore: &FetchSemaphore, -) -> crate::Result { +) -> crate::Result { let result = fetch_advanced( Method::GET, &format!("{MODRINTH_API_URL}user"), @@ -367,7 +352,6 @@ async fn fetch_info( Some(("Authorization", token)), None, semaphore, - &CredentialsStore(None), ) .await?; let value = serde_json::from_slice(&result)?; diff --git a/packages/app-lib/src/state/profiles.rs b/packages/app-lib/src/state/profiles.rs index 1d4cd09ac..f0d1b018f 100644 --- a/packages/app-lib/src/state/profiles.rs +++ b/packages/app-lib/src/state/profiles.rs @@ -1,38 +1,45 @@ use super::settings::{Hooks, MemorySettings, WindowSize}; -use crate::config::MODRINTH_API_URL; -use crate::data::DirectoryInfo; -use crate::event::emit::{emit_profile, emit_warning}; -use crate::event::ProfilePayloadType; -use crate::prelude::JavaVersion; -use crate::state::projects::Project; -use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType}; -use crate::util::fetch::{ - fetch, fetch_json, write, write_cached_icon, IoSemaphore, -}; -use crate::util::io::{self, IOError}; -use crate::State; -use chrono::{DateTime, Utc}; -use daedalus::get_hash; -use daedalus::modded::LoaderVersion; +use crate::util::fetch::{write_cached_icon, IoSemaphore}; +use crate::util::io::{self}; +use chrono::{DateTime, TimeZone, Utc}; +use dashmap::DashMap; use futures::prelude::*; -use notify::{RecommendedWatcher, RecursiveMode}; -use notify_debouncer_mini::Debouncer; -use reqwest::Method; use serde::{Deserialize, Serialize}; -use std::io::Cursor; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; -use uuid::Uuid; +use std::path::Path; -const PROFILE_JSON_PATH: &str = "profile.json"; +// Represent a Minecraft instance. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Profile { + pub path: String, + pub install_stage: ProfileInstallStage, -pub(crate) struct Profiles(pub HashMap); + pub name: String, + pub icon_path: Option, -#[derive( - Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq, -)] + pub game_version: String, + pub loader: ModLoader, + pub loader_version: Option, + + pub linked_data: Option, + + pub created: DateTime, + pub modified: DateTime, + pub last_played: Option>, + + pub submitted_time_played: u64, + pub recent_time_played: u64, + + pub java_path: Option, + pub extra_launch_args: Option>, + pub custom_env_vars: Option>, + + pub memory: Option, + pub force_fullscreen: Option, + pub game_resolution: Option, + pub hooks: Hooks, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ProfileInstallStage { /// Profile is installed @@ -42,237 +49,41 @@ pub enum ProfileInstallStage { /// Profile created for pack, but the pack hasn't been fully installed yet PackInstalling, /// Profile is not installed - #[default] NotInstalled, } -/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used -/// eg: for "a/b/c/profiles/My Mod", the ProfilePathId would be "My Mod" (a relative path) -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] -#[serde(transparent)] -pub struct ProfilePathId(PathBuf); - -impl ProfilePathId { - // Create a new ProfilePathId from a full file path - pub async fn from_fs_path(path: PathBuf) -> crate::Result { - let path: PathBuf = io::canonicalize(path)?; - let profiles_dir = io::canonicalize( - State::get().await?.directories.profiles_dir().await, - )?; - path.strip_prefix(profiles_dir) - .ok() - .and_then(|p| p.file_name()) - .ok_or_else(|| { - crate::ErrorKind::FSError(format!( - "Path {path:?} does not correspond to a profile", - path = path - )) - })?; - Ok(Self(path)) - } - - // Create a new ProfilePathId from a relative path - pub fn new(path: impl Into) -> Self { - ProfilePathId(path.into()) - } - - pub async fn get_full_path(&self) -> crate::Result { - let state = State::get().await?; - let profiles_dir = state.directories.profiles_dir().await; - Ok(profiles_dir.join(&self.0)) - } - - pub fn check_valid_utf(&self) -> crate::Result<&Self> { - self.0 - .to_str() - .ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?; - Ok(self) - } - - pub async fn profile_path(&self) -> crate::Result { - if let Some(p) = crate::profile::get(self, None).await? { - Ok(p.profile_id()) - } else { - Err(crate::ErrorKind::UnmanagedProfileError(self.to_string()) - .into()) +impl ProfileInstallStage { + pub fn as_str(&self) -> &'static str { + match *self { + Self::Installed => "installed", + Self::Installing => "installing", + Self::PackInstalling => "pack_installing", + Self::NotInstalled => "not_installed", } } -} -impl std::fmt::Display for ProfilePathId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.display().fmt(f) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] -#[serde(into = "RawProjectPath", from = "RawProjectPath")] -pub struct InnerProjectPathUnix(pub String); - -impl InnerProjectPathUnix { - pub fn get_topmost_two_components(&self) -> String { - self.to_string() - .split('/') - .take(2) - .collect::>() - .join("/") - } -} - -impl std::fmt::Display for InnerProjectPathUnix { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for InnerProjectPathUnix { - fn from(value: RawProjectPath) -> Self { - // Convert windows path to unix path. - // .mrpacks no longer generate windows paths, but this is here for backwards compatibility before this was fixed - // https://github.com/modrinth/theseus/issues/595 - InnerProjectPathUnix(value.0.replace('\\', "/")) - } -} -#[derive(Serialize, Deserialize)] -#[serde(transparent)] -struct RawProjectPath(pub String); - -impl From for RawProjectPath { - fn from(value: InnerProjectPathUnix) -> Self { - RawProjectPath(value.0) - } -} - -/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used -/// eg: for "a/b/c/profiles/My Mod/mods/myproj", the ProjectPathId would be "mods/myproj" -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] -#[serde(transparent)] -pub struct ProjectPathId(pub PathBuf); -impl ProjectPathId { - // Create a new ProjectPathId from a full file path - pub async fn from_fs_path(path: &PathBuf) -> crate::Result { - // This is avoiding dunce::canonicalize deliberately. On Windows, paths will always be convert to UNC, - // but this is ok because we are stripping that with the prefix. Using std::fs avoids different behaviors with dunce that - // come with too-long paths - let profiles_dir: PathBuf = std::fs::canonicalize( - State::get().await?.directories.profiles_dir().await, - )?; - let path: PathBuf = std::fs::canonicalize(path)?; - let path = path - .strip_prefix(profiles_dir) - .ok() - .map(|p| p.components().skip(1).collect::()) - .ok_or_else(|| { - crate::ErrorKind::FSError(format!( - "Path {path:?} does not correspond to a profile", - path = path - )) - })?; - Ok(Self(path)) - } - - pub async fn get_full_path( - &self, - profile: ProfilePathId, - ) -> crate::Result { - let profile_dir = profile.get_full_path().await?; - Ok(profile_dir.join(&self.0)) - } - - // Gets inner path in unix convention as a String - // ie: 'mods\myproj' -> 'mods/myproj' - // Used for exporting to mrpack, which should have a singular convention - pub fn get_inner_path_unix(&self) -> InnerProjectPathUnix { - InnerProjectPathUnix( - self.0 - .components() - .map(|c| c.as_os_str().to_string_lossy().to_string()) - .collect::>() - .join("/"), - ) - } - - // Create a new ProjectPathId from a relative path - pub fn new(path: &Path) -> Self { - ProjectPathId(PathBuf::from(path)) + pub fn from_str(val: &str) -> Self { + match val { + "installed" => Self::Installed, + "installing" => Self::Installing, + "pack_installing" => Self::PackInstalling, + "not_installed" => Self::NotInstalled, + _ => Self::NotInstalled, + } } } -// Represent a Minecraft instance. -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Profile { - pub uuid: Uuid, // todo: will be used in restructure to refer to profiles - #[serde(default)] - pub install_stage: ProfileInstallStage, - #[serde(default)] - pub path: PathBuf, // Relative path to the profile, to be used in ProfilePathId - pub metadata: ProfileMetadata, - #[serde(skip_serializing_if = "Option::is_none")] - pub java: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub memory: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub resolution: Option, - pub fullscreen: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hooks: Option, - pub projects: HashMap, - #[serde(default)] - pub modrinth_update_version: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ProfileMetadata { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub icon: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub icon_url: Option, - #[serde(default)] - pub groups: Vec, - - pub game_version: String, - #[serde(default)] - pub loader: ModLoader, - #[serde(skip_serializing_if = "Option::is_none")] - pub loader_version: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub linked_data: Option, - - #[serde(default)] - pub date_created: DateTime, - #[serde(default)] - pub date_modified: DateTime, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_played: Option>, - #[serde(default)] - pub submitted_time_played: u64, - #[serde(default)] - pub recent_time_played: u64, -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct LinkedData { - pub project_id: Option, - pub version_id: Option, + pub project_id: String, + pub version_id: String, - #[serde(default = "default_locked")] - pub locked: Option, + pub locked: bool, } -// Called if linked_data is present but locked is not -// Meaning this is a legacy profile, and we should consider it locked -pub fn default_locked() -> Option { - Some(true) -} - -#[derive( - Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default, -)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum ModLoader { - #[default] Vanilla, Forge, Fabric, @@ -280,20 +91,8 @@ pub enum ModLoader { NeoForge, } -impl std::fmt::Display for ModLoader { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match *self { - Self::Vanilla => "Vanilla", - Self::Forge => "Forge", - Self::Fabric => "Fabric", - Self::Quilt => "Quilt", - Self::NeoForge => "NeoForge", - }) - } -} - impl ModLoader { - pub(crate) fn as_api_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match *self { Self::Vanilla => "vanilla", Self::Forge => "forge", @@ -302,796 +101,622 @@ impl ModLoader { Self::NeoForge => "neoforge", } } -} -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct JavaSettings { - #[serde(skip_serializing_if = "Option::is_none")] - pub override_version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub extra_arguments: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub custom_env_args: Option>, -} - -impl Profile { - #[tracing::instrument] - pub async fn new( - uuid: Uuid, - name: String, - version: String, - ) -> crate::Result { - if name.trim().is_empty() { - return Err(crate::ErrorKind::InputError(String::from( - "Empty name for instance!", - )) - .into()); + pub fn as_meta_str(&self) -> &'static str { + match *self { + Self::Vanilla => "vanilla", + Self::Forge => "forge", + Self::Fabric => "fabric", + Self::Quilt => "quilt", + Self::NeoForge => "neo", } - - Ok(Self { - uuid, - install_stage: ProfileInstallStage::NotInstalled, - path: PathBuf::new().join(&name), - metadata: ProfileMetadata { - name, - icon: None, - icon_url: None, - groups: vec![], - game_version: version, - loader: ModLoader::Vanilla, - loader_version: None, - linked_data: None, - date_created: Utc::now(), - date_modified: Utc::now(), - last_played: None, - submitted_time_played: 0, - recent_time_played: 0, - }, - projects: HashMap::new(), - java: None, - memory: None, - resolution: None, - fullscreen: None, - hooks: None, - modrinth_update_version: None, - }) - } - - // Gets the ProfilePathId for this profile - #[inline] - pub fn profile_id(&self) -> ProfilePathId { - ProfilePathId::new(&self.path) } - #[tracing::instrument(skip(self, semaphore, icon))] - pub async fn set_icon<'a>( - &'a mut self, - cache_dir: &Path, - semaphore: &IoSemaphore, - icon: bytes::Bytes, - file_name: &str, - ) -> crate::Result<()> { - let file = - write_cached_icon(file_name, cache_dir, icon, semaphore).await?; - self.metadata.icon = Some(file); - self.metadata.date_modified = Utc::now(); - Ok(()) - } - - pub fn crash_task(path: ProfilePathId) { - tokio::task::spawn(async move { - let res = async { - let profile = crate::api::profile::get(&path, None).await?; - - if let Some(profile) = profile { - // Hide warning if profile is not yet installed - if profile.install_stage == ProfileInstallStage::Installed { - emit_warning(&format!("Profile {} has crashed! Visit the logs page to see a crash report.", profile.metadata.name)).await?; - } - } - - Ok::<(), crate::Error>(()) - } - .await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!( - "Unable to send crash report to frontend: {err}" - ) - } - }; - }); - } - - pub fn sync_projects_task(profile_path_id: ProfilePathId, force: bool) { - let span = tracing::span!( - tracing::Level::INFO, - "sync_projects_task", - ?profile_path_id, - ?force - ); - tokio::task::spawn(async move { - let res = async { - let _span = span.enter(); - let state = State::get().await?; - let profile = crate::api::profile::get(&profile_path_id, None).await?; - - if let Some(profile) = profile { - if profile.install_stage != ProfileInstallStage::PackInstalling || force { - let paths = profile.get_profile_full_project_paths().await?; - - let caches_dir = state.directories.caches_dir(); - let creds = state.credentials.read().await; - let projects = crate::state::infer_data_from_files( - profile.clone(), - paths, - caches_dir, - &state.io_semaphore, - &state.fetch_semaphore, - &creds, - ) - .await?; - drop(creds); - - let mut new_profiles = state.profiles.write().await; - if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) { - profile.projects = projects; - } - emit_profile( - profile.uuid, - &profile_path_id, - &profile.metadata.name, - ProfilePayloadType::Synced, - ) - .await?; - } - } else { - tracing::warn!( - "Unable to fetch single profile projects: path {profile_path_id} invalid", - ); - } - Ok::<(), crate::Error>(()) - }.await; - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!( - "Unable to fetch single profile projects: {err}" - ) - } - }; - }); - } - - // Get full path to profile - pub async fn get_profile_full_path(&self) -> crate::Result { - let state = State::get().await?; - let profiles_dir = state.directories.profiles_dir().await; - Ok(profiles_dir.join(&self.path)) - } - - /// Gets paths to projects as their full paths, not just their relative paths - pub async fn get_profile_full_project_paths( - &self, - ) -> crate::Result> { - let mut files = Vec::new(); - let profile_path = self.get_profile_full_path().await?; - let mut read_paths = |path: &str| { - let new_path = profile_path.join(path); - if new_path.exists() { - for subpath in std::fs::read_dir(&new_path) - .map_err(|e| IOError::with_path(e, &new_path))? - { - let subpath = subpath.map_err(IOError::from)?.path(); - if subpath.is_file() { - files.push(subpath); - } - } - } - Ok::<(), crate::Error>(()) - }; - - read_paths(ProjectType::Mod.get_folder())?; - read_paths(ProjectType::ShaderPack.get_folder())?; - read_paths(ProjectType::ResourcePack.get_folder())?; - read_paths(ProjectType::DataPack.get_folder())?; - - Ok(files) - } - - #[tracing::instrument(skip(watcher))] - #[theseus_macros::debug_pin] - pub async fn watch_fs( - profile_path: &Path, - watcher: &mut Debouncer, - ) -> crate::Result<()> { - async fn watch_path( - profile_path: &Path, - watcher: &mut Debouncer, - path: &str, - ) -> crate::Result<()> { - let path = profile_path.join(path); - - io::create_dir_all(&path).await?; - - watcher - .watcher() - .watch(&profile_path.join(path), RecursiveMode::Recursive)?; - - Ok(()) + pub fn from_str(val: &str) -> Self { + match val { + "vanilla" => Self::Vanilla, + "forge" => Self::Forge, + "fabric" => Self::Fabric, + "quilt" => Self::Quilt, + "neoforge" => Self::NeoForge, + _ => Self::Vanilla, } - - watch_path(profile_path, watcher, ProjectType::Mod.get_folder()) - .await?; - watch_path(profile_path, watcher, ProjectType::ShaderPack.get_folder()) - .await?; - watch_path( - profile_path, - watcher, - ProjectType::ResourcePack.get_folder(), - ) - .await?; - watch_path(profile_path, watcher, ProjectType::DataPack.get_folder()) - .await?; - watch_path(profile_path, watcher, "crash-reports").await?; - - Ok(()) } +} - #[tracing::instrument(skip(self))] - #[theseus_macros::debug_pin] - pub async fn add_project_version( - &self, - version_id: String, - ) -> crate::Result<(ProjectPathId, ModrinthVersion)> { - let state = State::get().await?; - let creds = state.credentials.read().await; - let version = fetch_json::( - Method::GET, - &format!("{MODRINTH_API_URL}version/{version_id}"), - None, - None, - &state.fetch_semaphore, - &creds, - ) - .await?; - drop(creds); - let file = if let Some(file) = version.files.iter().find(|x| x.primary) - { - file - } else if let Some(file) = version.files.first() { - file - } else { - return Err(crate::ErrorKind::InputError( - "No files for input version present!".to_string(), - ) - .into()); - }; - - let creds = state.credentials.read().await; - let bytes = fetch( - &file.url, - file.hashes.get("sha1").map(|x| &**x), - &state.fetch_semaphore, - &creds, +impl Profile { + pub async fn get( + path: &str, + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result> { + let res = sqlx::query!( + r#" + SELECT + path, install_stage, name, icon_path, + game_version, mod_loader, mod_loader_version, + linked_project_id, linked_version_id, locked, + created, modified, last_played, + submitted_time_played, recent_time_played, + override_java_path, + json(override_extra_launch_args) as "override_extra_launch_args!: serde_json::Value", json(override_custom_env_vars) as "override_custom_env_vars!: serde_json::Value", + override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y, + override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit + FROM profiles + WHERE path = $1 + "#, + path ) - .await?; - drop(creds); - let path = self - .add_project_bytes( - &file.filename, - bytes, - ProjectType::get_from_loaders(version.loaders.clone()), - ) + .fetch_optional(exec) .await?; - Ok((path, version)) - } - #[tracing::instrument(skip(self, bytes))] - #[theseus_macros::debug_pin] - pub async fn add_project_bytes( - &self, - file_name: &str, - bytes: bytes::Bytes, - project_type: Option, - ) -> crate::Result { - let project_type = if let Some(project_type) = project_type { - project_type - } else { - let cursor = Cursor::new(&*bytes); - - let mut archive = zip::ZipArchive::new(cursor).map_err(|_| { - crate::ErrorKind::InputError( - "Unable to infer project type for input file".to_string(), - ) - })?; - if archive.by_name("fabric.mod.json").is_ok() - || archive.by_name("quilt.mod.json").is_ok() - || archive.by_name("META-INF/mods.toml").is_ok() - || archive.by_name("mcmod.info").is_ok() + Ok(res.map(|x| Profile { + path: x.path, + install_stage: ProfileInstallStage::from_str(&x.install_stage), + name: x.name, + icon_path: x.icon_path, + game_version: x.game_version, + loader: ModLoader::from_str(&x.mod_loader), + loader_version: x.mod_loader_version, + linked_data: None, + created: Utc + .timestamp_opt(x.created, 0) + .single() + .unwrap_or_else(|| Utc::now()), + modified: Utc + .timestamp_opt(x.modified, 0) + .single() + .unwrap_or_else(|| Utc::now()), + last_played: x + .last_played + .and_then(|x| Utc.timestamp_opt(x, 0).single()), + submitted_time_played: x.submitted_time_played as u64, + recent_time_played: x.recent_time_played as u64, + java_path: x.override_java_path, + extra_launch_args: serde_json::from_value( + x.override_extra_launch_args, + ) + .ok(), + custom_env_vars: serde_json::from_value(x.override_custom_env_vars) + .ok(), + memory: x + .override_mc_memory_max + .map(|x| MemorySettings { maximum: x as u32 }), + force_fullscreen: x.override_mc_force_fullscreen.map(|x| x == 1), + game_resolution: if let Some(x_res) = + x.override_mc_game_resolution_x { - ProjectType::Mod - } else if archive.by_name("pack.mcmeta").is_ok() { - if archive.file_names().any(|x| x.starts_with("data/")) { - ProjectType::DataPack + if let Some(y_res) = x.override_mc_game_resolution_y { + Some(WindowSize(x_res as u16, y_res as u16)) } else { - ProjectType::ResourcePack + None } - } else { - return Err(crate::ErrorKind::InputError( - "Unable to infer project type for input file".to_string(), - ) - .into()); - } - }; - - let state = State::get().await?; - let relative_name = PathBuf::new() - .join(project_type.get_folder()) - .join(file_name); - let file_path = self - .get_profile_full_path() - .await? - .join(relative_name.clone()); - let project_path_id = ProjectPathId::new(&relative_name); - write(&file_path, &bytes, &state.io_semaphore).await?; - - let hash = get_hash(bytes).await?; - { - let mut profiles = state.profiles.write().await; - - if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { - profile.projects.insert( - project_path_id.clone(), - Project { - sha512: hash, - disabled: false, - metadata: ProjectMetadata::Unknown, - file_name: file_name.to_string(), - }, - ); - profile.metadata.date_modified = Utc::now(); - } - } - - Ok(project_path_id) - } - - /// Toggle a project's disabled state. - #[tracing::instrument(skip(self))] - #[theseus_macros::debug_pin] - pub async fn toggle_disable_project( - &self, - relative_path: &ProjectPathId, - ) -> crate::Result { - let state = State::get().await?; - if let Some(mut project) = { - let mut profiles: tokio::sync::RwLockWriteGuard<'_, Profiles> = - state.profiles.write().await; - - if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { - profile.projects.remove(relative_path) } else { None - } - } { - // Get relative path from former ProjectPathId - let relative_path = relative_path.0.to_path_buf(); - let mut new_path = relative_path.clone(); - - if relative_path - .extension() - .map_or(false, |ext| ext == "disabled") - { - project.disabled = false; - new_path.set_file_name( - relative_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .replace(".disabled", ""), + }, + hooks: Hooks { + pre_launch: x.override_hook_pre_launch, + wrapper: x.override_hook_wrapper, + post_exit: x.override_hook_post_exit, + }, + })) + } + + pub async fn get_all( + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result> { + // TODO: remove duplicated code + let res = sqlx::query!( + r#" + SELECT + path, install_stage, name, icon_path, + game_version, mod_loader, mod_loader_version, + linked_project_id, linked_version_id, locked, + created, modified, last_played, + submitted_time_played, recent_time_played, + override_java_path, + json(override_extra_launch_args) as "override_extra_launch_args!: serde_json::Value", json(override_custom_env_vars) as "override_custom_env_vars!: serde_json::Value", + override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y, + override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit + FROM profiles + "# + ) + .fetch(exec) + .try_fold(DashMap::new(), |acc, x| { + acc.insert( + x.path.clone(), + Profile { + path: x.path, + install_stage: ProfileInstallStage::from_str(&x.install_stage), + name: x.name, + icon_path: x.icon_path, + game_version: x.game_version, + loader: ModLoader::from_str(&x.mod_loader), + loader_version: x.mod_loader_version, + linked_data: if let Some(project_id) = x.linked_project_id { + if let Some(version_id) = x.linked_version_id { + if let Some(locked) = x.locked { + Some(LinkedData { + project_id, + version_id, + locked: locked == 1, + }) + } else { None } + } else { None } + } else { None }, + created: Utc.timestamp_opt(x.created, 0).single().unwrap_or_else(Utc::now), + modified: Utc.timestamp_opt(x.modified, 0).single().unwrap_or_else(Utc::now), + last_played: x.last_played.and_then(|x| Utc.timestamp_opt(x, 0).single()), + submitted_time_played: x.submitted_time_played as u64, + recent_time_played: x.recent_time_played as u64, + java_path: x.override_java_path, + extra_launch_args: serde_json::from_value(x + .override_extra_launch_args).ok(), + custom_env_vars: serde_json::from_value(x.override_custom_env_vars).ok(), + memory: x.override_mc_memory_max.map(|x| MemorySettings { + maximum: x as u32, + }), + force_fullscreen: x.override_mc_force_fullscreen.map(|x| x == 1), + game_resolution: if let Some(x_res) = x.override_mc_game_resolution_x { + if let Some(y_res) = x.override_mc_game_resolution_y { + Some(WindowSize( + x_res as u16, + y_res as u16, + )) + } else { None } + + } else { None }, + hooks: Hooks { + pre_launch: x.override_hook_pre_launch, + wrapper: x.override_hook_wrapper, + post_exit: x.override_hook_post_exit, + }, + }, ); - } else { - new_path.set_file_name(format!( - "{}.disabled", - relative_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - )); - project.disabled = true; - } - - let true_path = - self.get_profile_full_path().await?.join(&relative_path); - let true_new_path = - self.get_profile_full_path().await?.join(&new_path); - io::rename(&true_path, &true_new_path).await?; - let new_project_path_id = ProjectPathId::new(&new_path); - - let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { - profile - .projects - .insert(new_project_path_id.clone(), project); - profile.metadata.date_modified = Utc::now(); - } + async move { Ok(acc) } + }) + .await?; - Ok(new_project_path_id) - } else { - Err(crate::ErrorKind::InputError(format!( - "Project path does not exist: {:?}", - relative_path - )) - .into()) - } + Ok(res) } - pub async fn remove_project( + pub async fn upsert( &self, - relative_path: &ProjectPathId, - dont_remove_arr: Option, + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result<()> { - let state = State::get().await?; - if self.projects.contains_key(relative_path) { - io::remove_file( - self.get_profile_full_path() - .await? - .join(relative_path.0.clone()), + let install_stage = self.install_stage.as_str(); + let mod_loader = self.loader.as_str(); + + let linked_data_project_id = + self.linked_data.as_ref().map(|x| x.project_id.clone()); + let linked_data_version_id = + self.linked_data.as_ref().map(|x| x.version_id.clone()); + let linked_data_locked = self.linked_data.as_ref().map(|x| x.locked); + + let created = self.created.timestamp(); + let modified = self.modified.timestamp(); + let last_played = self.last_played.map(|x| x.timestamp()); + + let submitted_time_played = self.submitted_time_played as i64; + let recent_time_played = self.recent_time_played as i64; + + let memory_max = self.memory.map(|x| x.maximum); + + let game_resolution_x = self.game_resolution.map(|x| x.0); + let game_resolution_y = self.game_resolution.map(|x| x.1); + + let extra_launch_args = serde_json::to_string(&self.extra_launch_args)?; + let custom_env_vars = serde_json::to_string(&self.custom_env_vars)?; + + sqlx::query!( + " + INSERT INTO profiles ( + path, install_stage, name, icon_path, + game_version, mod_loader, mod_loader_version, + linked_project_id, linked_version_id, locked, + created, modified, last_played, + submitted_time_played, recent_time_played, + override_java_path, override_extra_launch_args, override_custom_env_vars, + override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y, + override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit ) - .await?; - if !dont_remove_arr.unwrap_or(false) { - let mut profiles = state.profiles.write().await; - - if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { - profile.projects.remove(relative_path); - profile.metadata.date_modified = Utc::now(); - } - } - } else { - // If we are removing a project that doesn't exist, allow it to pass through without error, but warn - tracing::warn!( - "Attempted to remove non-existent project: {:?}", - relative_path - ); - } - - Ok(()) - } -} - -impl Profiles { - #[tracing::instrument(skip(file_watcher))] - #[theseus_macros::debug_pin] - pub async fn init( - dirs: &DirectoryInfo, - file_watcher: &mut Debouncer, - ) -> crate::Result { - let mut profiles = HashMap::new(); - let profiles_dir = dirs.profiles_dir().await; - io::create_dir_all(&&profiles_dir).await?; - - file_watcher - .watcher() - .watch(&profiles_dir, RecursiveMode::NonRecursive)?; - - let mut entries = io::read_dir(&dirs.profiles_dir().await).await?; - while let Some(entry) = - entries.next_entry().await.map_err(IOError::from)? - { - let path = entry.path(); - if path.is_dir() { - let prof = match Self::read_profile_from_dir(&path, dirs).await - { - Ok(prof) => Some(prof), - Err(err) => { - tracing::warn!( - "Error loading profile: {err}. Skipping..." - ); - None - } - }; - - if let Some(profile) = prof { - // Clear out modrinth_logs of all files in profiles folder (these are legacy) - // TODO: should be removed in a future build - let modrinth_logs = path.join("modrinth_logs"); - if modrinth_logs.exists() { - let _ = std::fs::remove_dir_all(modrinth_logs); - } - - let path = io::canonicalize(path)?; - Profile::watch_fs(&path, file_watcher).await?; - profiles.insert(profile.profile_id(), profile); - } - } - } - - Ok(Self(profiles)) - } - - #[tracing::instrument] - #[theseus_macros::debug_pin] - pub async fn update_projects() { - let res = async { - let state = State::get().await?; - - // profile, child paths - let mut files: Vec<(Profile, Vec)> = Vec::new(); - { - let profiles = state.profiles.read().await; - for (_profile_path, profile) in profiles.0.iter() { - let paths = - profile.get_profile_full_project_paths().await?; - - files.push((profile.clone(), paths)); - } - } - - let caches_dir = state.directories.caches_dir(); - future::try_join_all(files.into_iter().map( - |(profile, files)| async { - let profile_name = profile.profile_id(); - let creds = state.credentials.read().await; - let inferred = super::projects::infer_data_from_files( - profile, - files, - caches_dir.clone(), - &state.io_semaphore, - &state.fetch_semaphore, - &creds, - ) - .await?; - drop(creds); - - let mut new_profiles = state.profiles.write().await; - if let Some(profile) = new_profiles.0.get_mut(&profile_name) - { - profile.projects = inferred; - } - drop(new_profiles); - - Ok::<(), crate::Error>(()) - }, - )) - .await?; - - { - let profiles = state.profiles.read().await; - profiles.sync().await?; - } - - Ok::<(), crate::Error>(()) - } - .await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!("Unable to fetch profile projects: {err}") - } - }; - } - - #[tracing::instrument] - #[theseus_macros::debug_pin] - pub async fn update_modrinth_versions() { - let res = async { - let state = State::get().await?; - // Temporarily store all profiles that have modrinth linked data - let mut modrinth_updatables: Vec<(ProfilePathId, String)> = - Vec::new(); - { - let profiles = state.profiles.read().await; - for (profile_path, profile) in profiles.0.iter() { - if let Some(linked_data) = &profile.metadata.linked_data { - if let Some(linked_project) = &linked_data.project_id { - modrinth_updatables.push(( - profile_path.clone(), - linked_project.clone(), - )); - } - } - } - } - - // Fetch online from Modrinth each latest version - future::try_join_all(modrinth_updatables.into_iter().map( - |(profile_path, linked_project)| { - let state = state.clone(); - async move { - let creds = state.credentials.read().await; - let versions: Vec = fetch_json( - Method::GET, - &format!( - "{}project/{}/version", - MODRINTH_API_URL, - linked_project.clone() - ), - None, - None, - &state.fetch_semaphore, - &creds, - ) - .await?; - drop(creds); - - // Versions are pre-sorted in labrinth (by versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));) - // so we can just take the first one for which the loader matches - let mut new_profiles = state.profiles.write().await; - if let Some(profile) = - new_profiles.0.get_mut(&profile_path) - { - let loader = profile.metadata.loader; - let recent_version = versions.iter().find(|x| { - x.loaders - .contains(&loader.as_api_str().to_string()) - }); - if let Some(recent_version) = recent_version { - profile.modrinth_update_version = - Some(recent_version.id.clone()); - } else { - profile.modrinth_update_version = None; - } - } - drop(new_profiles); - - Ok::<(), crate::Error>(()) - } - }, - )) - .await?; - - { - let profiles = state.profiles.read().await; - profiles.sync().await?; - } - - Ok::<(), crate::Error>(()) - } - .await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!("Unable to update modrinth versions: {err}") - } - }; - } - - #[tracing::instrument(skip(self, profile))] - #[theseus_macros::debug_pin] - pub async fn insert( - &mut self, - profile: Profile, - no_watch: bool, - ) -> crate::Result<&Self> { - emit_profile( - profile.uuid, - &profile.profile_id(), - &profile.metadata.name, - ProfilePayloadType::Added, - ) - .await?; - - if !no_watch { - let state = State::get().await?; - let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs( - &profile.get_profile_full_path().await?, - &mut file_watcher, + VALUES ( + $1, $2, $3, $4, + $5, $6, $7, + $8, $9, $10, + $11, $12, $13, + $14, $15, + $16, $17, $18, + $19, $20, $21, $22, + $23, $24, $25 ) + ON CONFLICT (path) DO UPDATE SET + install_stage = $2, + name = $3, + icon_path = $4, + + game_version = $5, + mod_loader = $6, + mod_loader_version = $7, + + linked_project_id = $8, + linked_version_id = $9, + locked = $10, + + created = $11, + modified = $12, + last_played = $13, + + submitted_time_played = $14, + recent_time_played = $15, + + override_java_path = $16, + override_extra_launch_args = jsonb($17), + override_custom_env_vars = jsonb($18), + override_mc_memory_max = $19, + override_mc_force_fullscreen = $20, + override_mc_game_resolution_x = $21, + override_mc_game_resolution_y = $22, + + override_hook_pre_launch = $23, + override_hook_wrapper = $24, + override_hook_post_exit = $25 + ", + self.path, + install_stage, + self.name, + self.icon_path, + self.game_version, + mod_loader, + self.loader_version, + linked_data_project_id, + linked_data_version_id, + linked_data_locked, + created, + modified, + last_played, + submitted_time_played, + recent_time_played, + self.java_path, + extra_launch_args, + custom_env_vars, + memory_max, + self.force_fullscreen, + game_resolution_x, + game_resolution_y, + self.hooks.pre_launch, + self.hooks.wrapper, + self.hooks.post_exit, + ) + .execute(exec) .await?; - } - let profile_name = profile.profile_id(); - profile_name.check_valid_utf()?; - self.0.insert(profile_name, profile); - Ok(self) + Ok(()) } - #[tracing::instrument(skip(self))] pub async fn remove( - &mut self, - profile_path: &ProfilePathId, - ) -> crate::Result> { - let profile = self.0.remove(profile_path); + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> crate::Result<()> { + let path = crate::api::profile::get_full_path(&self.path).await?; - let path = profile_path.get_full_path().await?; if path.exists() { io::remove_dir_all(&path).await?; } - Ok(profile) - } - - #[tracing::instrument(skip_all)] - pub async fn sync(&self) -> crate::Result<&Self> { - let _state = State::get().await?; - stream::iter(self.0.iter()) - .map(Ok::<_, crate::Error>) - .try_for_each_concurrent(None, |(_, profile)| async move { - let json = serde_json::to_vec(&profile)?; - - let json_path = profile - .get_profile_full_path() - .await? - .join(PROFILE_JSON_PATH); - - io::write(&json_path, &json).await?; - Ok::<_, crate::Error>(()) - }) - .await?; + sqlx::query!( + " + DELETE FROM profiles + WHERE path = $1 + ", + self.path + ) + .execute(&mut **transaction) + .await?; - Ok(self) + Ok(()) } - async fn read_profile_from_dir( - path: &Path, - dirs: &DirectoryInfo, - ) -> crate::Result { - let json = io::read(&path.join(PROFILE_JSON_PATH)).await?; - let mut profile = serde_json::from_slice::(&json)?; - - // Get name from stripped path - profile.path = - PathBuf::from(path.strip_prefix(dirs.profiles_dir().await)?); - - Ok(profile) + #[tracing::instrument(skip(self, semaphore, icon))] + pub async fn set_icon<'a>( + &'a mut self, + cache_dir: &Path, + semaphore: &IoSemaphore, + icon: bytes::Bytes, + file_name: &str, + ) -> crate::Result<()> { + let file = + write_cached_icon(file_name, cache_dir, icon, semaphore).await?; + self.icon_path = Some(file.to_string_lossy().to_string()); + self.modified = Utc::now(); + Ok(()) } - pub fn sync_available_profiles_task(profile_path_id: ProfilePathId) { - tokio::task::spawn(async move { - let span = tracing::span!( - tracing::Level::INFO, - "sync_available_profiles_task" - ); - let res = async { - let _span = span.enter(); - let state = State::get().await?; - let dirs = &state.directories; - let mut profiles = state.profiles.write().await; - - if let Some(profile) = profiles.0.get_mut(&profile_path_id) { - if !profile.get_profile_full_path().await?.exists() { - // if path exists in the state but no longer in the filesystem, remove it from the state list - emit_profile( - profile.uuid, - &profile_path_id, - &profile.metadata.name, - ProfilePayloadType::Removed, - ) - .await?; - tracing::debug!("Removed!"); - profiles.0.remove(&profile_path_id); - } - } else if profile_path_id.get_full_path().await?.exists() { - // if it exists in the filesystem but no longer in the state, add it to the state list - profiles - .insert( - Self::read_profile_from_dir( - &profile_path_id.get_full_path().await?, - dirs, - ) - .await?, - false, - ) - .await?; - Profile::sync_projects_task(profile_path_id, false); - } - Ok::<(), crate::Error>(()) - } - .await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!("Unable to fetch all profiles: {err}") - } - }; - }); - } + // pub fn crash_task(path: ProfilePathId) { + // tokio::task::spawn(async move { + // let res = async { + // let profile = crate::api::profile::get(&path).await?; + // + // if let Some(profile) = profile { + // // Hide warning if profile is not yet installed + // if profile.install_stage == ProfileInstallStage::Installed { + // emit_warning(&format!("Profile {} has crashed! Visit the logs page to see a crash report.", profile.metadata.name)).await?; + // } + // } + // + // Ok::<(), crate::Error>(()) + // } + // .await; + // + // match res { + // Ok(()) => {} + // Err(err) => { + // tracing::warn!( + // "Unable to send crash report to frontend: {err}" + // ) + // } + // }; + // }); + // } + + // #[tracing::instrument(skip(watcher))] + // #[theseus_macros::debug_pin] + // pub async fn watch_fs( + // profile_path: &Path, + // watcher: &mut Debouncer, + // ) -> crate::Result<()> { + // async fn watch_path( + // profile_path: &Path, + // watcher: &mut Debouncer, + // path: &str, + // ) -> crate::Result<()> { + // let path = profile_path.join(path); + // + // io::create_dir_all(&path).await?; + // + // watcher + // .watcher() + // .watch(&profile_path.join(path), RecursiveMode::Recursive)?; + // + // Ok(()) + // } + // + // watch_path(profile_path, watcher, ProjectType::Mod.get_folder()) + // .await?; + // watch_path(profile_path, watcher, ProjectType::ShaderPack.get_folder()) + // .await?; + // watch_path( + // profile_path, + // watcher, + // ProjectType::ResourcePack.get_folder(), + // ) + // .await?; + // watch_path(profile_path, watcher, ProjectType::DataPack.get_folder()) + // .await?; + // watch_path(profile_path, watcher, "crash-reports").await?; + // + // Ok(()) + // } + + // #[tracing::instrument(skip(self))] + // #[theseus_macros::debug_pin] + // pub async fn add_project_version( + // &self, + // version_id: String, + // ) -> crate::Result<(ProjectPathId, ModrinthVersion)> { + // let state = State::get().await?; + // let creds = state.credentials.read().await; + // let version = fetch_json::( + // Method::GET, + // &format!("{MODRINTH_API_URL}version/{version_id}"), + // None, + // None, + // &state.fetch_semaphore, + // &creds, + // ) + // .await?; + // drop(creds); + // let file = if let Some(file) = version.files.iter().find(|x| x.primary) + // { + // file + // } else if let Some(file) = version.files.first() { + // file + // } else { + // return Err(crate::ErrorKind::InputError( + // "No files for input version present!".to_string(), + // ) + // .into()); + // }; + // + // let creds = state.credentials.read().await; + // let bytes = fetch( + // &file.url, + // file.hashes.get("sha1").map(|x| &**x), + // &state.fetch_semaphore, + // &creds, + // ) + // .await?; + // drop(creds); + // let path = self + // .add_project_bytes( + // &file.filename, + // bytes, + // ProjectType::get_from_loaders(version.loaders.clone()), + // ) + // .await?; + // Ok((path, version)) + // } + + // #[tracing::instrument(skip(self, bytes))] + // #[theseus_macros::debug_pin] + // pub async fn add_project_bytes( + // &self, + // file_name: &str, + // bytes: bytes::Bytes, + // project_type: Option, + // ) -> crate::Result { + // let project_type = if let Some(project_type) = project_type { + // project_type + // } else { + // let cursor = Cursor::new(&*bytes); + // + // let mut archive = zip::ZipArchive::new(cursor).map_err(|_| { + // crate::ErrorKind::InputError( + // "Unable to infer project type for input file".to_string(), + // ) + // })?; + // if archive.by_name("fabric.mod.json").is_ok() + // || archive.by_name("quilt.mod.json").is_ok() + // || archive.by_name("META-INF/mods.toml").is_ok() + // || archive.by_name("mcmod.info").is_ok() + // { + // ProjectType::Mod + // } else if archive.by_name("pack.mcmeta").is_ok() { + // if archive.file_names().any(|x| x.starts_with("data/")) { + // ProjectType::DataPack + // } else { + // ProjectType::ResourcePack + // } + // } else { + // return Err(crate::ErrorKind::InputError( + // "Unable to infer project type for input file".to_string(), + // ) + // .into()); + // } + // }; + // + // let state = State::get().await?; + // let relative_name = PathBuf::new() + // .join(project_type.get_folder()) + // .join(file_name); + // let file_path = self + // .get_profile_full_path() + // .await? + // .join(relative_name.clone()); + // let project_path_id = ProjectPathId::new(&relative_name); + // write(&file_path, &bytes, &state.io_semaphore).await?; + // + // let hash = get_hash(bytes).await?; + // { + // let mut profiles = state.profiles.write().await; + // + // if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { + // profile.projects.insert( + // project_path_id.clone(), + // Project { + // sha512: hash, + // disabled: false, + // metadata: ProjectMetadata::Unknown, + // file_name: file_name.to_string(), + // }, + // ); + // profile.metadata.date_modified = Utc::now(); + // } + // } + // + // Ok(project_path_id) + // } + // + // /// Toggle a project's disabled state. + // #[tracing::instrument(skip(self))] + // #[theseus_macros::debug_pin] + // pub async fn toggle_disable_project( + // &self, + // relative_path: &ProjectPathId, + // ) -> crate::Result { + // let state = State::get().await?; + // if let Some(mut project) = { + // let mut profiles: tokio::sync::RwLockWriteGuard<'_, Profiles> = + // state.profiles.write().await; + // + // if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { + // profile.projects.remove(relative_path) + // } else { + // None + // } + // } { + // // Get relative path from former ProjectPathId + // let relative_path = relative_path.0.to_path_buf(); + // let mut new_path = relative_path.clone(); + // + // if relative_path + // .extension() + // .map_or(false, |ext| ext == "disabled") + // { + // project.disabled = false; + // new_path.set_file_name( + // relative_path + // .file_name() + // .unwrap_or_default() + // .to_string_lossy() + // .replace(".disabled", ""), + // ); + // } else { + // new_path.set_file_name(format!( + // "{}.disabled", + // relative_path + // .file_name() + // .unwrap_or_default() + // .to_string_lossy() + // )); + // project.disabled = true; + // } + // + // let true_path = + // self.get_profile_full_path().await?.join(&relative_path); + // let true_new_path = + // self.get_profile_full_path().await?.join(&new_path); + // io::rename(&true_path, &true_new_path).await?; + // + // let new_project_path_id = ProjectPathId::new(&new_path); + // + // let mut profiles = state.profiles.write().await; + // if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { + // profile + // .projects + // .insert(new_project_path_id.clone(), project); + // profile.metadata.date_modified = Utc::now(); + // } + // + // Ok(new_project_path_id) + // } else { + // Err(crate::ErrorKind::InputError(format!( + // "Project path does not exist: {:?}", + // relative_path + // )) + // .into()) + // } + // } + // + // pub async fn remove_project( + // &self, + // relative_path: &ProjectPathId, + // dont_remove_arr: Option, + // ) -> crate::Result<()> { + // let state = State::get().await?; + // if self.projects.contains_key(relative_path) { + // io::remove_file( + // self.get_profile_full_path() + // .await? + // .join(relative_path.0.clone()), + // ) + // .await?; + // if !dont_remove_arr.unwrap_or(false) { + // let mut profiles = state.profiles.write().await; + // + // if let Some(profile) = profiles.0.get_mut(&self.profile_id()) { + // profile.projects.remove(relative_path); + // profile.metadata.date_modified = Utc::now(); + // } + // } + // } else { + // // If we are removing a project that doesn't exist, allow it to pass through without error, but warn + // tracing::warn!( + // "Attempted to remove non-existent project: {:?}", + // relative_path + // ); + // } + // + // Ok(()) + // } } diff --git a/packages/app-lib/src/state/projects.rs b/packages/app-lib/src/state/projects.rs deleted file mode 100644 index 888b244af..000000000 --- a/packages/app-lib/src/state/projects.rs +++ /dev/null @@ -1,807 +0,0 @@ -//! Project management + inference - -use crate::config::MODRINTH_API_URL; -use crate::state::{CredentialsStore, ModrinthUser, Profile}; -use crate::util::fetch::{ - fetch_json, write_cached_icon, FetchSemaphore, IoSemaphore, -}; -use crate::util::io::IOError; - -use async_zip::tokio::read::fs::ZipFileReader; -use chrono::{DateTime, Utc}; -use futures::StreamExt; -use reqwest::Method; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use sha2::Digest; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use tokio::io::AsyncReadExt; - -use super::ProjectPathId; - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "lowercase")] -pub enum ProjectType { - Mod, - DataPack, - ResourcePack, - ShaderPack, -} - -impl ProjectType { - pub fn get_from_loaders(loaders: Vec) -> Option { - if loaders - .iter() - .any(|x| ["fabric", "forge", "quilt"].contains(&&**x)) - { - Some(ProjectType::Mod) - } else if loaders.iter().any(|x| x == "datapack") { - Some(ProjectType::DataPack) - } else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) { - Some(ProjectType::ShaderPack) - } else if loaders - .iter() - .any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x)) - { - Some(ProjectType::ResourcePack) - } else { - None - } - } - - pub fn get_from_parent_folder(path: PathBuf) -> Option { - // Get parent folder - let path = path.parent()?.file_name()?; - match path.to_str()? { - "mods" => Some(ProjectType::Mod), - "datapacks" => Some(ProjectType::DataPack), - "resourcepacks" => Some(ProjectType::ResourcePack), - "shaderpacks" => Some(ProjectType::ShaderPack), - _ => None, - } - } - - pub fn get_name(&self) -> &'static str { - match self { - ProjectType::Mod => "mod", - ProjectType::DataPack => "datapack", - ProjectType::ResourcePack => "resourcepack", - ProjectType::ShaderPack => "shaderpack", - } - } - - pub fn get_folder(&self) -> &'static str { - match self { - ProjectType::Mod => "mods", - ProjectType::DataPack => "datapacks", - ProjectType::ResourcePack => "resourcepacks", - ProjectType::ShaderPack => "shaderpacks", - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Project { - pub sha512: String, - pub disabled: bool, - pub metadata: ProjectMetadata, - pub file_name: String, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ModrinthProject { - pub id: String, - pub slug: Option, - pub project_type: String, - pub team: String, - pub title: String, - pub description: String, - pub body: String, - - pub published: DateTime, - pub updated: DateTime, - - pub client_side: SideType, - pub server_side: SideType, - - pub downloads: u32, - pub followers: u32, - - pub categories: Vec, - pub additional_categories: Vec, - pub game_versions: Vec, - pub loaders: Vec, - - pub versions: Vec, - - pub icon_url: Option, -} - -/// A specific version of a project -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ModrinthVersion { - pub id: String, - pub project_id: String, - pub author_id: String, - - pub featured: bool, - - pub name: String, - pub version_number: String, - pub changelog: String, - pub changelog_url: Option, - - pub date_published: DateTime, - pub downloads: u32, - pub version_type: String, - - pub files: Vec, - pub dependencies: Vec, - pub game_versions: Vec, - pub loaders: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ModrinthVersionFile { - pub hashes: HashMap, - pub url: String, - pub filename: String, - pub primary: bool, - pub size: u32, - pub file_type: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Dependency { - pub version_id: Option, - pub project_id: Option, - pub file_name: Option, - pub dependency_type: DependencyType, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ModrinthTeamMember { - pub team_id: String, - pub user: ModrinthUser, - pub role: String, - pub ordering: i64, -} - -#[derive(Serialize, Deserialize, Copy, Clone, Debug)] -#[serde(rename_all = "lowercase")] -pub enum DependencyType { - Required, - Optional, - Incompatible, - Embedded, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub enum SideType { - Required, - Optional, - Unsupported, - Unknown, -} - -#[derive(Serialize, Deserialize, Copy, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum FileType { - RequiredResourcePack, - OptionalResourcePack, - Unknown, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ProjectMetadata { - Modrinth { - project: Box, - version: Box, - members: Vec, - update_version: Option>, - incompatible: bool, - }, - Inferred { - title: Option, - description: Option, - authors: Vec, - version: Option, - icon: Option, - project_type: Option, - }, - Unknown, -} - -#[tracing::instrument(skip(io_semaphore))] -#[theseus_macros::debug_pin] -async fn read_icon_from_file( - icon_path: Option, - cache_dir: &Path, - path: &PathBuf, - io_semaphore: &IoSemaphore, -) -> crate::Result> { - if let Some(icon_path) = icon_path { - // we have to repoen the zip twice here :( - let zip_file_reader = ZipFileReader::new(path).await; - if let Ok(zip_file_reader) = zip_file_reader { - // Get index of icon file and open it - let zip_index_option = - zip_file_reader.file().entries().iter().position(|f| { - f.filename().as_str().unwrap_or_default() == icon_path - }); - if let Some(zip_index) = zip_index_option { - let mut bytes = Vec::new(); - if zip_file_reader - .reader_with_entry(zip_index) - .await? - .read_to_end_checked(&mut bytes) - .await - .is_ok() - { - let bytes = bytes::Bytes::from(bytes); - let path = write_cached_icon( - &icon_path, - cache_dir, - bytes, - io_semaphore, - ) - .await?; - - return Ok(Some(path)); - } - } - } - } - - Ok(None) -} - -// Creates Project data from the existing files in the file system, for a given Profile -// Paths must be the full paths to the files in the FS, and not the relative paths -// eg: with get_profile_full_project_paths -#[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))] -#[theseus_macros::debug_pin] -pub async fn infer_data_from_files( - profile: Profile, - paths: Vec, - cache_dir: PathBuf, - io_semaphore: &IoSemaphore, - fetch_semaphore: &FetchSemaphore, - credentials: &CredentialsStore, -) -> crate::Result> { - let mut file_path_hashes = HashMap::new(); - - for path in paths { - if !path.exists() { - continue; - } - if let Some(ext) = path.extension() { - // Ignore txt configuration files - if ext == "txt" { - continue; - } - } - - let mut file = tokio::fs::File::open(path.clone()) - .await - .map_err(|e| IOError::with_path(e, &path))?; - - let mut buffer = [0u8; 4096]; // Buffer to read chunks - let mut hasher = sha2::Sha512::new(); // Hasher - - loop { - let bytes_read = - file.read(&mut buffer).await.map_err(IOError::from)?; - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - - let hash = format!("{:x}", hasher.finalize()); - file_path_hashes.insert(hash, path.clone()); - } - - let files_url = format!("{}version_files", MODRINTH_API_URL); - let updates_url = format!("{}version_files/update", MODRINTH_API_URL); - let (files, update_versions) = tokio::try_join!( - fetch_json::>( - Method::POST, - &files_url, - None, - Some(json!({ - "hashes": file_path_hashes.keys().collect::>(), - "algorithm": "sha512", - })), - fetch_semaphore, - credentials, - ), - fetch_json::>( - Method::POST, - &updates_url, - None, - Some(json!({ - "hashes": file_path_hashes.keys().collect::>(), - "algorithm": "sha512", - "loaders": [profile.metadata.loader], - "game_versions": [profile.metadata.game_version] - })), - fetch_semaphore, - credentials, - ) - )?; - - let projects: Vec = fetch_json( - Method::GET, - &format!( - "{}projects?ids={}", - MODRINTH_API_URL, - serde_json::to_string( - &files - .values() - .map(|x| x.project_id.clone()) - .collect::>() - )? - ), - None, - None, - fetch_semaphore, - credentials, - ) - .await?; - - let teams: Vec = fetch_json::< - Vec>, - >( - Method::GET, - &format!( - "{}teams?ids={}", - MODRINTH_API_URL, - serde_json::to_string( - &projects.iter().map(|x| x.team.clone()).collect::>() - )? - ), - None, - None, - fetch_semaphore, - credentials, - ) - .await? - .into_iter() - .flatten() - .collect(); - - let mut return_projects: Vec<(PathBuf, Project)> = Vec::new(); - let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new(); - - for (hash, path) in file_path_hashes { - if let Some(version) = files.get(&hash) { - if let Some(project) = - projects.iter().find(|x| version.project_id == x.id) - { - let file_name = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - return_projects.push(( - path, - Project { - disabled: file_name.ends_with(".disabled"), - metadata: ProjectMetadata::Modrinth { - project: Box::new(project.clone()), - version: Box::new(version.clone()), - members: teams - .iter() - .filter(|x| x.team_id == project.team) - .cloned() - .collect::>(), - update_version: if let Some(value) = - update_versions.get(&hash) - { - if value.id != version.id { - Some(Box::new(value.clone())) - } else { - None - } - } else { - None - }, - incompatible: !version.loaders.contains( - &profile - .metadata - .loader - .as_api_str() - .to_string(), - ) || version - .game_versions - .contains(&profile.metadata.game_version), - }, - sha512: hash, - file_name, - }, - )); - continue; - } - } - - further_analyze_projects.push((hash, path)); - } - - for (hash, path) in further_analyze_projects { - let file_name = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - let zip_file_reader = if let Ok(zip_file_reader) = - ZipFileReader::new(path.clone()).await - { - zip_file_reader - } else { - return_projects.push(( - path.clone(), - Project { - sha512: hash, - disabled: file_name.ends_with(".disabled"), - metadata: ProjectMetadata::Unknown, - file_name, - }, - )); - continue; - }; - - // Forge - let zip_index_option = - zip_file_reader.file().entries().iter().position(|f| { - f.filename().as_str().unwrap_or_default() - == "META-INF/mods.toml" - }); - if let Some(index) = zip_index_option { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct ForgeModInfo { - pub mods: Vec, - } - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct ForgeMod { - mod_id: String, - version: Option, - display_name: Option, - description: Option, - logo_file: Option, - authors: Option, - } - - let mut file_str = String::new(); - if zip_file_reader - .reader_with_entry(index) - .await? - .read_to_string_checked(&mut file_str) - .await - .is_ok() - { - if let Ok(pack) = toml::from_str::(&file_str) { - if let Some(pack) = pack.mods.first() { - let icon = read_icon_from_file( - pack.logo_file.clone(), - &cache_dir, - &path, - io_semaphore, - ) - .await?; - - return_projects.push(( - path.clone(), - Project { - sha512: hash, - disabled: file_name.ends_with(".disabled"), - file_name, - metadata: ProjectMetadata::Inferred { - title: Some( - pack.display_name - .clone() - .unwrap_or_else(|| { - pack.mod_id.clone() - }), - ), - description: pack.description.clone(), - authors: pack - .authors - .clone() - .map(|x| vec![x]) - .unwrap_or_default(), - version: pack.version.clone(), - icon, - project_type: Some("mod".to_string()), - }, - }, - )); - continue; - } - } - } - } - - // Forge - let zip_index_option = - zip_file_reader.file().entries().iter().position(|f| { - f.filename().as_str().unwrap_or_default() == "mcmod.info" - }); - if let Some(index) = zip_index_option { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct ForgeMod { - modid: String, - name: String, - description: Option, - version: Option, - author_list: Option>, - logo_file: Option, - } - - let mut file_str = String::new(); - if zip_file_reader - .reader_with_entry(index) - .await? - .read_to_string_checked(&mut file_str) - .await - .is_ok() - { - if let Ok(pack) = serde_json::from_str::(&file_str) { - let icon = read_icon_from_file( - pack.logo_file, - &cache_dir, - &path, - io_semaphore, - ) - .await?; - - return_projects.push(( - path.clone(), - Project { - sha512: hash, - disabled: file_name.ends_with(".disabled"), - file_name, - metadata: ProjectMetadata::Inferred { - title: Some(if pack.name.is_empty() { - pack.modid - } else { - pack.name - }), - description: pack.description, - authors: pack.author_list.unwrap_or_default(), - version: pack.version, - icon, - project_type: Some("mod".to_string()), - }, - }, - )); - continue; - } - } - } - - // Fabric - let zip_index_option = - zip_file_reader.file().entries().iter().position(|f| { - f.filename().as_str().unwrap_or_default() == "fabric.mod.json" - }); - if let Some(index) = zip_index_option { - #[derive(Deserialize)] - #[serde(untagged)] - enum FabricAuthor { - String(String), - Object { name: String }, - } - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct FabricMod { - id: String, - version: String, - name: Option, - description: Option, - authors: Vec, - icon: Option, - } - - let mut file_str = String::new(); - if zip_file_reader - .reader_with_entry(index) - .await? - .read_to_string_checked(&mut file_str) - .await - .is_ok() - { - if let Ok(pack) = serde_json::from_str::(&file_str) { - let icon = read_icon_from_file( - pack.icon, - &cache_dir, - &path, - io_semaphore, - ) - .await?; - - return_projects.push(( - path.clone(), - Project { - sha512: hash, - disabled: file_name.ends_with(".disabled"), - file_name, - metadata: ProjectMetadata::Inferred { - title: Some(pack.name.unwrap_or(pack.id)), - description: pack.description, - authors: pack - .authors - .into_iter() - .map(|x| match x { - FabricAuthor::String(name) => name, - FabricAuthor::Object { name } => name, - }) - .collect(), - version: Some(pack.version), - icon, - project_type: Some("mod".to_string()), - }, - }, - )); - continue; - } - } - } - - // Quilt - let zip_index_option = - zip_file_reader.file().entries().iter().position(|f| { - f.filename().as_str().unwrap_or_default() == "quilt.mod.json" - }); - if let Some(index) = zip_index_option { - #[derive(Deserialize)] - struct QuiltMetadata { - pub name: Option, - pub description: Option, - pub contributors: Option>, - pub icon: Option, - } - #[derive(Deserialize)] - struct QuiltMod { - id: String, - version: String, - metadata: Option, - } - - let mut file_str = String::new(); - if zip_file_reader - .reader_with_entry(index) - .await? - .read_to_string_checked(&mut file_str) - .await - .is_ok() - { - if let Ok(pack) = serde_json::from_str::(&file_str) { - let icon = read_icon_from_file( - pack.metadata.as_ref().and_then(|x| x.icon.clone()), - &cache_dir, - &path, - io_semaphore, - ) - .await?; - - return_projects.push(( - path.clone(), - Project { - sha512: hash, - disabled: file_name.ends_with(".disabled"), - file_name, - metadata: ProjectMetadata::Inferred { - title: Some( - pack.metadata - .as_ref() - .and_then(|x| x.name.clone()) - .unwrap_or(pack.id), - ), - description: pack - .metadata - .as_ref() - .and_then(|x| x.description.clone()), - authors: pack - .metadata - .map(|x| { - x.contributors - .unwrap_or_default() - .keys() - .cloned() - .collect() - }) - .unwrap_or_default(), - version: Some(pack.version), - icon, - project_type: Some("mod".to_string()), - }, - }, - )); - continue; - } - } - } - - // Other - let zip_index_option = - zip_file_reader.file().entries().iter().position(|f| { - f.filename().as_str().unwrap_or_default() == "pack.mcmeta" - }); - if let Some(index) = zip_index_option { - #[derive(Deserialize)] - struct Pack { - description: Option, - } - - let mut file_str = String::new(); - if zip_file_reader - .reader_with_entry(index) - .await? - .read_to_string_checked(&mut file_str) - .await - .is_ok() - { - if let Ok(pack) = serde_json::from_str::(&file_str) { - let icon = read_icon_from_file( - Some("pack.png".to_string()), - &cache_dir, - &path, - io_semaphore, - ) - .await?; - - // Guess the project type from the filepath - let project_type = - ProjectType::get_from_parent_folder(path.clone()); - return_projects.push(( - path.clone(), - Project { - sha512: hash, - disabled: file_name.ends_with(".disabled"), - file_name, - metadata: ProjectMetadata::Inferred { - title: None, - description: pack.description, - authors: Vec::new(), - version: None, - icon, - project_type: project_type - .map(|x| x.get_name().to_string()), - }, - }, - )); - continue; - } - } - } - - return_projects.push(( - path.clone(), - Project { - sha512: hash, - disabled: file_name.ends_with(".disabled"), - file_name, - metadata: ProjectMetadata::Unknown, - }, - )); - } - - // Project paths should be relative - let mut corrected_hashmap = HashMap::new(); - let mut stream = tokio_stream::iter(return_projects); - while let Some((h, v)) = stream.next().await { - let h = ProjectPathId::from_fs_path(&h).await?; - corrected_hashmap.insert(h, v); - } - - Ok(corrected_hashmap) -} diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs index 8d999c902..98c1ae0cf 100644 --- a/packages/app-lib/src/state/settings.rs +++ b/packages/app-lib/src/state/settings.rs @@ -1,127 +1,158 @@ //! Theseus settings file use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; -use tokio::fs; - -use super::{DirectoryInfo, JavaGlobals}; - -// TODO: convert to semver? -const CURRENT_FORMAT_VERSION: u32 = 1; // Types /// Global Theseus settings #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Settings { - pub theme: Theme, - pub memory: MemorySettings, - #[serde(default)] - pub force_fullscreen: bool, - pub game_resolution: WindowSize, - pub custom_java_args: Vec, - pub custom_env_args: Vec<(String, String)>, - pub java_globals: JavaGlobals, - pub hooks: Hooks, pub max_concurrent_downloads: usize, pub max_concurrent_writes: usize, - pub version: u32, + + pub theme: Theme, + pub default_page: DefaultPage, pub collapsed_navigation: bool, - #[serde(default)] - pub disable_discord_rpc: bool, - #[serde(default)] - pub hide_on_process: bool, - #[serde(default)] + pub advanced_rendering: bool, pub native_decorations: bool, - #[serde(default)] - pub default_page: DefaultPage, - #[serde(default)] + + pub telemetry: bool, + pub discord_rpc: bool, pub developer_mode: bool, - #[serde(default)] - pub opt_out_analytics: bool, - #[serde(default)] - pub advanced_rendering: bool, - #[serde(default)] - pub fully_onboarded: bool, - #[serde(default = "DirectoryInfo::get_initial_settings_dir")] - pub loaded_config_dir: Option, + + pub onboarded: bool, + + pub extra_launch_args: Vec, + pub custom_env_vars: Vec<(String, String)>, + pub memory: MemorySettings, + pub force_fullscreen: bool, + pub game_resolution: WindowSize, + pub hide_on_process_start: bool, + pub hooks: Hooks, } impl Settings { - #[tracing::instrument] - pub async fn init(file: &Path) -> crate::Result { - let mut rescued = false; - - let settings = if file.exists() { - let loaded_settings = fs::read(&file) - .await - .map_err(|err| { - crate::ErrorKind::FSError(format!( - "Error reading settings file: {err}" - )) - .as_error() - }) - .and_then(|it| { - serde_json::from_slice::(&it) - .map_err(crate::Error::from) - }); - // settings is corrupted. Back up the file and create a new one - if let Err(ref err) = loaded_settings { - tracing::error!("Failed to load settings file: {err}. "); - let backup_file = file.with_extension("json.bak"); - tracing::error!("Corrupted settings file will be backed up as {}, and a new settings file will be created.", backup_file.display()); - let _ = fs::rename(file, backup_file).await; - rescued = true; - } - loaded_settings.ok() - } else { - None - }; - - if let Some(settings) = settings { - Ok(settings) - } else { - // Create new settings file - let settings = Self { - theme: Theme::Dark, - memory: MemorySettings::default(), - force_fullscreen: false, - game_resolution: WindowSize::default(), - custom_java_args: Vec::new(), - custom_env_args: Vec::new(), - java_globals: JavaGlobals::new(), - hooks: Hooks::default(), - max_concurrent_downloads: 10, - max_concurrent_writes: 10, - version: CURRENT_FORMAT_VERSION, - collapsed_navigation: false, - disable_discord_rpc: false, - hide_on_process: false, - native_decorations: false, - default_page: DefaultPage::Home, - developer_mode: false, - opt_out_analytics: false, - advanced_rendering: true, - fully_onboarded: rescued, // If we rescued the settings file, we should consider the user fully onboarded - - // By default, the config directory is the same as the settings directory - loaded_config_dir: DirectoryInfo::get_initial_settings_dir(), - }; - if rescued { - settings.sync(file).await?; - } - Ok(settings) - } + pub async fn get( + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result { + let res = sqlx::query!( + " + SELECT + max_concurrent_writes, max_concurrent_downloads, + theme, default_page, collapsed_navigation, advanced_rendering, native_decorations, + discord_rpc, developer_mode, telemetry, + onboarded, + json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars, + mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start, + hook_pre_launch, hook_wrapper, hook_post_exit + FROM settings + " + ) + .fetch_one(exec) + .await?; + + Ok(Self { + max_concurrent_downloads: res.max_concurrent_downloads as usize, + max_concurrent_writes: res.max_concurrent_writes as usize, + theme: Theme::from_string(&res.theme), + default_page: DefaultPage::from_string(&res.default_page), + collapsed_navigation: res.collapsed_navigation == 1, + advanced_rendering: res.advanced_rendering == 1, + native_decorations: res.native_decorations == 1, + telemetry: res.telemetry == 1, + discord_rpc: res.discord_rpc == 1, + developer_mode: res.developer_mode == 1, + onboarded: res.onboarded == 1, + extra_launch_args: res + .extra_launch_args + .and_then(|x| serde_json::from_str(&x).ok()) + .unwrap_or_default(), + custom_env_vars: res + .custom_env_vars + .and_then(|x| serde_json::from_str(&x).ok()) + .unwrap_or_default(), + memory: MemorySettings { + maximum: res.mc_memory_max as u32, + }, + force_fullscreen: res.mc_force_fullscreen == 1, + game_resolution: WindowSize( + res.mc_game_resolution_x as u16, + res.mc_game_resolution_y as u16, + ), + hide_on_process_start: res.hide_on_process_start == 1, + hooks: Hooks { + pre_launch: res.hook_pre_launch, + wrapper: res.hook_wrapper, + post_exit: res.hook_post_exit, + }, + }) } - #[tracing::instrument(skip(self))] - pub async fn sync(&self, to: &Path) -> crate::Result<()> { - fs::write(to, serde_json::to_vec(self)?) - .await - .map_err(|err| { - crate::ErrorKind::FSError(format!( - "Error saving settings to file: {err}" - )) - .as_error() - })?; + pub async fn update( + &self, + exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> crate::Result<()> { + let max_concurrent_writes = self.max_concurrent_writes as i32; + let max_concurrent_downloads = self.max_concurrent_downloads as i32; + let theme = self.theme.as_str(); + let default_page = self.default_page.as_str(); + let extra_launch_args = serde_json::to_string(&self.extra_launch_args)?; + let custom_env_vars = serde_json::to_string(&self.custom_env_vars)?; + + sqlx::query!( + " + UPDATE settings + SET + max_concurrent_writes = $1, + max_concurrent_downloads = $2, + + theme = $3, + default_page = $4, + collapsed_navigation = $5, + advanced_rendering = $6, + native_decorations = $7, + + discord_rpc = $8, + developer_mode = $9, + telemetry = $10, + + onboarded = $11, + + extra_launch_args = jsonb($12), + custom_env_vars = jsonb($13), + mc_memory_max = $14, + mc_force_fullscreen = $15, + mc_game_resolution_x = $16, + mc_game_resolution_y = $17, + hide_on_process_start = $18, + + hook_pre_launch = $19, + hook_wrapper = $20, + hook_post_exit = $21 + ", + max_concurrent_writes, + max_concurrent_downloads, + theme, + default_page, + self.collapsed_navigation, + self.advanced_rendering, + self.native_decorations, + self.discord_rpc, + self.developer_mode, + self.telemetry, + self.onboarded, + extra_launch_args, + custom_env_vars, + self.memory.maximum, + self.force_fullscreen, + self.game_resolution.0, + self.game_resolution.1, + self.hide_on_process_start, + self.hooks.pre_launch, + self.hooks.wrapper, + self.hooks.post_exit, + ) + .execute(exec) + .await?; + Ok(()) } } @@ -135,37 +166,40 @@ pub enum Theme { Oled, } +impl Theme { + pub fn as_str(&self) -> &'static str { + match self { + Theme::Dark => "dark", + Theme::Light => "light", + Theme::Oled => "oled", + } + } + + pub fn from_string(string: &str) -> Theme { + match string { + "dark" => Theme::Dark, + "light" => Theme::Light, + "oled" => Theme::Oled, + _ => Theme::Dark, + } + } +} + /// Minecraft memory settings #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct MemorySettings { pub maximum: u32, } -impl Default for MemorySettings { - fn default() -> Self { - Self { maximum: 2048 } - } -} - /// Game window size #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct WindowSize(pub u16, pub u16); -impl Default for WindowSize { - fn default() -> Self { - Self(854, 480) - } -} - /// Game initialization hooks -#[derive(Serialize, Deserialize, Debug, Clone, Default)] -#[serde(default)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Hooks { - #[serde(skip_serializing_if = "Option::is_none")] pub pre_launch: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub wrapper: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub post_exit: Option, } @@ -176,8 +210,19 @@ pub enum DefaultPage { Library, } -impl Default for DefaultPage { - fn default() -> Self { - Self::Home +impl DefaultPage { + pub fn as_str(&self) -> &'static str { + match self { + DefaultPage::Home => "home", + DefaultPage::Library => "library", + } + } + + pub fn from_string(string: &str) -> Self { + match string { + "home" => Self::Home, + "library" => Self::Library, + _ => Self::Home, + } } } diff --git a/packages/app-lib/src/state/tags.rs b/packages/app-lib/src/state/tags.rs deleted file mode 100644 index 4db64e4d7..000000000 --- a/packages/app-lib/src/state/tags.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::path::PathBuf; - -use reqwest::Method; -use serde::{Deserialize, Serialize}; - -use crate::config::MODRINTH_API_URL; -use crate::data::DirectoryInfo; -use crate::state::CredentialsStore; -use crate::util::fetch::{ - fetch_json, read_json, write, FetchSemaphore, IoSemaphore, -}; - -// Serializeable struct for all tags to be fetched together by the frontend -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Tags { - pub categories: Vec, - pub loaders: Vec, - pub game_versions: Vec, - pub donation_platforms: Vec, - pub report_types: Vec, -} - -impl Tags { - #[tracing::instrument(skip(io_semaphore, fetch_semaphore))] - #[theseus_macros::debug_pin] - pub async fn init( - dirs: &DirectoryInfo, - fetch_online: bool, - io_semaphore: &IoSemaphore, - fetch_semaphore: &FetchSemaphore, - credentials: &CredentialsStore, - ) -> crate::Result { - let mut tags = None; - let tags_path = dirs.caches_meta_dir().await.join("tags.json"); - let tags_path_backup = - dirs.caches_meta_dir().await.join("tags.json.bak"); - - if let Ok(tags_json) = read_json::(&tags_path, io_semaphore).await - { - tags = Some(tags_json); - } else if fetch_online { - match Self::fetch(fetch_semaphore, credentials).await { - Ok(tags_fetch) => tags = Some(tags_fetch), - Err(err) => { - tracing::warn!("Unable to fetch launcher tags: {err}") - } - } - } else if let Ok(tags_json) = - read_json::(&tags_path_backup, io_semaphore).await - { - tags = Some(tags_json); - std::fs::copy(&tags_path_backup, &tags_path).map_err(|err| { - crate::ErrorKind::FSError(format!( - "Error restoring tags backup: {err}" - )) - .as_error() - })?; - } - - if let Some(tags_data) = tags { - write(&tags_path, &serde_json::to_vec(&tags_data)?, io_semaphore) - .await?; - write( - &tags_path_backup, - &serde_json::to_vec(&tags_data)?, - io_semaphore, - ) - .await?; - - Ok(tags_data) - } else { - Err(crate::ErrorKind::NoValueFor(String::from("launcher tags")) - .as_error()) - } - } - - #[tracing::instrument] - #[theseus_macros::debug_pin] - pub async fn update() { - let res = async { - let state = crate::State::get().await?; - - let creds = state.credentials.read().await; - let tags_fetch = - Tags::fetch(&state.fetch_semaphore, &creds).await?; - drop(creds); - - let tags_path = - state.directories.caches_meta_dir().await.join("tags.json"); - let tags_path_backup = state - .directories - .caches_meta_dir() - .await - .join("tags.json.bak"); - if tags_path.exists() { - std::fs::copy(&tags_path, &tags_path_backup).unwrap(); - } - - write( - &tags_path, - &serde_json::to_vec(&tags_fetch)?, - &state.io_semaphore, - ) - .await?; - - let mut old_tags = state.tags.write().await; - *old_tags = tags_fetch; - - Ok::<(), crate::Error>(()) - } - .await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!("Unable to update launcher tags: {err}") - } - }; - } - - // Checks the database for categories tag, returns a Vec::new() if it doesnt exist, otherwise returns the categories - #[tracing::instrument(skip(self))] - pub fn get_categories(&self) -> Vec { - self.categories.clone() - } - - // Checks the database for loaders tag, returns a Vec::new() if it doesnt exist, otherwise returns the loaders - #[tracing::instrument(skip(self))] - pub fn get_loaders(&self) -> Vec { - self.loaders.clone() - } - - // Checks the database for game_versions tag, returns a Vec::new() if it doesnt exist, otherwise returns the game_versions - #[tracing::instrument(skip(self))] - pub fn get_game_versions(&self) -> Vec { - self.game_versions.clone() - } - - // Checks the database for donation_platforms tag, returns a Vec::new() if it doesnt exist, otherwise returns the donation_platforms - #[tracing::instrument(skip(self))] - pub fn get_donation_platforms(&self) -> Vec { - self.donation_platforms.clone() - } - - // Checks the database for report_types tag, returns a Vec::new() if it doesnt exist, otherwise returns the report_types - #[tracing::instrument(skip(self))] - pub fn get_report_types(&self) -> Vec { - self.report_types.clone() - } - - // Gets all tags together as a serializable bundle - #[tracing::instrument(skip(self))] - pub fn get_tag_bundle(&self) -> Tags { - self.clone() - } - - // Fetches the tags from the Modrinth API and stores them in the database - pub async fn fetch( - semaphore: &FetchSemaphore, - credentials: &CredentialsStore, - ) -> crate::Result { - let categories = format!("{MODRINTH_API_URL}tag/category"); - let loaders = format!("{MODRINTH_API_URL}tag/loader"); - let game_versions = format!("{MODRINTH_API_URL}tag/game_version"); - let donation_platforms = - format!("{MODRINTH_API_URL}tag/donation_platform"); - let report_types = format!("{MODRINTH_API_URL}tag/report_type"); - - let categories_fut = fetch_json::>( - Method::GET, - &categories, - None, - None, - semaphore, - credentials, - ); - let loaders_fut = fetch_json::>( - Method::GET, - &loaders, - None, - None, - semaphore, - credentials, - ); - let game_versions_fut = fetch_json::>( - Method::GET, - &game_versions, - None, - None, - semaphore, - credentials, - ); - let donation_platforms_fut = fetch_json::>( - Method::GET, - &donation_platforms, - None, - None, - semaphore, - credentials, - ); - let report_types_fut = fetch_json::>( - Method::GET, - &report_types, - None, - None, - semaphore, - credentials, - ); - - let ( - categories, - loaders, - game_versions, - donation_platforms, - report_types, - ) = tokio::try_join!( - categories_fut, - loaders_fut, - game_versions_fut, - donation_platforms_fut, - report_types_fut - )?; - - Ok(Self { - categories, - loaders, - game_versions, - donation_platforms, - report_types, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Category { - pub name: String, - pub project_type: String, - pub header: String, - pub icon: PathBuf, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Loader { - pub name: String, - pub icon: PathBuf, - pub supported_project_types: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DonationPlatform { - pub short: String, - pub name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GameVersion { - pub version: String, - pub version_type: String, - pub date: String, - pub major: bool, -} diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index f31282f5e..2bb5f3f13 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -42,19 +42,8 @@ pub async fn fetch( url: &str, sha1: Option<&str>, semaphore: &FetchSemaphore, - credentials: &CredentialsStore, ) -> crate::Result { - fetch_advanced( - Method::GET, - url, - sha1, - None, - None, - None, - semaphore, - credentials, - ) - .await + fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore).await } #[tracing::instrument(skip(json_body, semaphore))] @@ -64,22 +53,13 @@ pub async fn fetch_json( sha1: Option<&str>, json_body: Option, semaphore: &FetchSemaphore, - credentials: &CredentialsStore, ) -> crate::Result where T: DeserializeOwned, { - let result = fetch_advanced( - method, - url, - sha1, - json_body, - None, - None, - semaphore, - credentials, - ) - .await?; + let result = + fetch_advanced(method, url, sha1, json_body, None, None, semaphore) + .await?; let value = serde_json::from_slice(&result)?; Ok(value) } @@ -96,7 +76,6 @@ pub async fn fetch_advanced( header: Option<(&str, &str)>, loading_bar: Option<(&LoadingBarId, f64)>, semaphore: &FetchSemaphore, - credentials: &CredentialsStore, ) -> crate::Result { let io_semaphore = semaphore.0.read().await; let _permit = io_semaphore.acquire().await?; @@ -112,11 +91,12 @@ pub async fn fetch_advanced( req = req.header(header.0, header.1); } - if url.starts_with("https://cdn.modrinth.com") { - if let Some(creds) = &credentials.0 { - req = req.header("Authorization", &creds.session); - } - } + // TODO: add back with db creds + // if url.starts_with("https://cdn.modrinth.com") { + // if let Some(creds) = &credentials.0 { + // req = req.header("Authorization", &creds.session); + // } + // } let result = req.send().await; match result { @@ -202,7 +182,7 @@ pub async fn fetch_mirrors( } for (index, mirror) in mirrors.iter().enumerate() { - let result = fetch(mirror, sha1, semaphore, credentials).await; + let result = fetch(mirror, sha1, semaphore).await; if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) { return result; diff --git a/packages/app-lib/src/util/jre.rs b/packages/app-lib/src/util/jre.rs index 6c571424b..3f8e5eced 100644 --- a/packages/app-lib/src/util/jre.rs +++ b/packages/app-lib/src/util/jre.rs @@ -1,6 +1,6 @@ use super::io; +use crate::state::JavaVersion; use futures::prelude::*; -use serde::{Deserialize, Serialize}; use std::env; use std::path::PathBuf; use std::process::Command; @@ -14,13 +14,6 @@ use winreg::{ RegKey, }; -#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)] -pub struct JavaVersion { - pub path: String, - pub version: String, - pub architecture: String, -} - // Entrypoint function (Windows) // Returns a Vec of unique JavaVersions from the PATH, Windows Registry Keys and common Java locations #[cfg(target_os = "windows")] @@ -317,12 +310,17 @@ pub async fn check_java_at_filepath(path: &Path) -> Option { // Extract version info from it if let Some(arch) = java_arch { if let Some(version) = java_version { - let path = java.to_string_lossy().to_string(); - return Some(JavaVersion { - path, - version: version.to_string(), - architecture: arch.to_string(), - }); + if let Ok((_, major_version)) = + extract_java_majorminor_version(version) + { + let path = java.to_string_lossy().to_string(); + return Some(JavaVersion { + major_version, + path, + version: version.to_string(), + architecture: arch.to_string(), + }); + } } } None