feat: desktop crate is ready to be fixed
This commit is contained in:
parent
f939169aa6
commit
cb1c8d54de
102 changed files with 1992 additions and 15574 deletions
1
.clippy.toml
Normal file
1
.clippy.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
cognitive-complexity-threshold = 30
|
||||
|
|
@ -12,3 +12,12 @@ use_try_shorthand = false
|
|||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
unstable_features = true
|
||||
deprecated = "warn"
|
||||
deprecated_in_future = "forbid"
|
||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unused_allocation = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_must_use = "deny"
|
||||
unused_qualifications = "warn"
|
||||
|
|
|
|||
2278
Cargo.lock
generated
2278
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
143
Cargo.toml
143
Cargo.toml
|
|
@ -1,5 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"apps/desktop",
|
||||
"packages/core",
|
||||
"packages/entity",
|
||||
"packages/gamemode",
|
||||
|
|
@ -10,7 +11,10 @@ members = [
|
|||
resolver = "2" # virtual workspace resolver so we don't need a root package
|
||||
|
||||
[workspace.package]
|
||||
authors = [ "Polyfrost", "Polyfrost Contributors" ]
|
||||
authors = [
|
||||
"Polyfrost",
|
||||
"Polyfrost Contributors"
|
||||
]
|
||||
documentation = "https://docs.polyfrost.org/onelauncher"
|
||||
edition = "2024"
|
||||
homepage = "https://polyfrost.org/projects/onelauncher"
|
||||
|
|
@ -28,67 +32,74 @@ onelauncher_macro = { path = "./packages/macro" }
|
|||
onelauncher_migration = { path = "./packages/migration" }
|
||||
serde_json_path_to_error = { path = "./packages/serde_json_path_to_error" }
|
||||
|
||||
# core
|
||||
interpulse = { version = "=1.2.2", features = [ "specta" ] }
|
||||
|
||||
# tauri
|
||||
tauri = { version = "2.0", features = [
|
||||
tauri = { version = "=2.5.1", features = [
|
||||
"rustls-tls",
|
||||
"tracing",
|
||||
"specta",
|
||||
"uuid",
|
||||
"protocol-asset",
|
||||
] }
|
||||
tauri-build = { version = "2.0" }
|
||||
tauri-plugin-clipboard-manager = { version = "2.0" }
|
||||
tauri-plugin-deep-link = { version = "2.0" }
|
||||
tauri-plugin-dialog = { version = "2.0" }
|
||||
tauri-plugin-shell = { version = "2.0" }
|
||||
tauri-plugin-single-instance = { version = "2.0" }
|
||||
tauri-plugin-updater = { version = "2.0" }
|
||||
tauri-plugin-window-state = { version = "2.0" }
|
||||
|
||||
# core
|
||||
interpulse = { version = "=1.2.2", features = [ "specta" ] }
|
||||
tauri-build = { version = "=2.2.0" }
|
||||
tauri-plugin-clipboard-manager = { version = "=2.2.2" }
|
||||
tauri-plugin-deep-link = { version = "=2.2.1" }
|
||||
tauri-plugin-dialog = { version = "=2.2.1" }
|
||||
tauri-plugin-single-instance = { version = "=2.2.3" }
|
||||
tauri-plugin-updater = { version = "=2.7.1" }
|
||||
tauri-plugin-window-state = { version = "=2.2.2" }
|
||||
|
||||
# code gen
|
||||
specta = { version = "2.0.0-rc.20", features = [
|
||||
strum = { version = "=0.27.1", features = [ "derive" ] }
|
||||
specta = { version = "=2.0.0-rc.22", features = [
|
||||
"chrono",
|
||||
"uuid",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"function",
|
||||
"derive",
|
||||
"time",
|
||||
"tokio"
|
||||
] }
|
||||
specta-typescript = { version = "=0.0.7" }
|
||||
tauri-specta = { version = "2.0.0-rc.20", features = [ "javascript", "typescript", "derive" ] }
|
||||
|
||||
# security
|
||||
iota_stronghold = { version = "2.1" }
|
||||
zeroize = { version = "1.8", features = [ "zeroize_derive" ] }
|
||||
specta-typescript = { version = "=0.0.9" }
|
||||
tauri-specta = { version = "=2.0.0-rc.21", features = [ "derive" ] }
|
||||
|
||||
# logging
|
||||
tracing = { version = "0.1" }
|
||||
tracing-appender = { version = "0.2" }
|
||||
tracing-error = { version = "0.2" }
|
||||
tracing-subscriber = { version = "0.3", features = [ "chrono", "env-filter" ] }
|
||||
tracing-subscriber = { version = "0.3", features = [
|
||||
"chrono",
|
||||
"env-filter"
|
||||
] }
|
||||
|
||||
# serialization & text
|
||||
base64 = { version = "0.22" }
|
||||
byteorder = { version = "1.5.0" }
|
||||
base64 = { version = "=0.22.1" }
|
||||
byteorder = { version = "=1.5.0" }
|
||||
chrono = { version = "0.4", features = [ "serde" ] }
|
||||
nom = { version = "7.1" }
|
||||
p256 = { version = "0.13.2", features = [ "ecdsa" ] }
|
||||
rand = { version = "0.8.5" }
|
||||
regex = { version = "1.10" }
|
||||
nom = { version = "=8.0.0" }
|
||||
rand = { version = "0.8" }
|
||||
regex = { version = "1.11" }
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
serde_json = { version = "1.0" }
|
||||
serde_with = { version = "=3.12.0", features = [ "chrono_0_4", "json", "macros" ] }
|
||||
strum = { version = "0.26", features = [ "derive" ] }
|
||||
serde_json = { version = "=1.0" }
|
||||
serde_with = { version = "=3.12.0", features = [
|
||||
"chrono_0_4",
|
||||
"json",
|
||||
"macros"
|
||||
] }
|
||||
toml = { version = "0.8" }
|
||||
url = { version = "2.5.2" }
|
||||
uuid = { version = "1.10", features = [ "serde", "v4" ] }
|
||||
url = { version = "2.5" }
|
||||
uuid = { version = "1.16", features = [
|
||||
"serde",
|
||||
"v4"
|
||||
] }
|
||||
|
||||
# network
|
||||
async-tungstenite = { version = "0.28", features = [ "tokio-runtime", "tokio-rustls-webpki-roots" ] }
|
||||
# async-tungstenite = { version = "0.28", features = [
|
||||
# "tokio-runtime",
|
||||
# "tokio-rustls-webpki-roots"
|
||||
# ] }
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"json",
|
||||
"stream",
|
||||
|
|
@ -102,30 +113,31 @@ reqwest = { version = "0.12", default-features = false, features = [
|
|||
"gzip",
|
||||
"brotli",
|
||||
] }
|
||||
webbrowser = { version = "1.0" }
|
||||
webbrowser = { version = "=1.0.4" }
|
||||
|
||||
# io
|
||||
async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] }
|
||||
async_zip = { version = "0.0.17", features = [ "full" ] }
|
||||
bytes = { version = "1.7" }
|
||||
dirs = { version = "5.0" }
|
||||
dunce = { version = "1.0.5" }
|
||||
flate2 = { version = "1.0" }
|
||||
notify = { version = "6.1", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.4", default-features = false }
|
||||
sanitize-filename = { version = "0.6" }
|
||||
tempfile = { version = "3.19" }
|
||||
tokio = { version = "1.40", features = [ "full" ] }
|
||||
tokio-stream = { version = "0.1.16", features = [ "fs" ] }
|
||||
async-compression = { version = "=0.4.23", features = [
|
||||
"flate2",
|
||||
"tokio"
|
||||
] }
|
||||
async_zip = { version = "=0.0.17", features = [ "full" ] }
|
||||
bytes = { version = "=1.10.1" }
|
||||
dirs = { version = "=6.0.0" }
|
||||
dunce = { version = "=1.0.5" }
|
||||
flate2 = { version = "=1.1.1" }
|
||||
sanitize-filename = { version = "=0.6.0" }
|
||||
tempfile = { version = "=3.19.1" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tokio-stream = { version = "0.1", features = [ "fs" ] }
|
||||
tokio-util = { version = "0.7", features = [ "compat" ] }
|
||||
zip = { version = "2.2.0" }
|
||||
|
||||
# crypto
|
||||
digest = { version = "0.10" }
|
||||
md-5 = { version = "0.10" }
|
||||
murmur2 = { version = "0.1.0" }
|
||||
murmur2 = { version = "=0.1.0" }
|
||||
sha1 = { version = "0.10" }
|
||||
sha2 = { version = "0.10" }
|
||||
p256 = { version = "0.13", features = [ "ecdsa" ] }
|
||||
|
||||
# db
|
||||
sea-orm = { version = "1.1", features = [
|
||||
|
|
@ -145,23 +157,29 @@ sea-orm-migration = { version = "1.1", features = [
|
|||
] }
|
||||
|
||||
# utils
|
||||
anyhow = { version = "1.0" }
|
||||
async-trait = { version = "0.1" }
|
||||
cc = { version = "1.1" }
|
||||
discord-rich-presence = { version = "0.2" }
|
||||
anyhow = { version = "1" }
|
||||
async-trait = { version = "=0.1.88" }
|
||||
cc = { version = "1" }
|
||||
discord-rich-presence = { version = "=0.2.5" }
|
||||
futures = { version = "0.3" }
|
||||
futures-lite = { version = "2.6" }
|
||||
indicatif = { version = "0.17" }
|
||||
libc = { version = "0.2" }
|
||||
merge = { version = "0.2" }
|
||||
sysinfo = { version = "0.31", features = [ "serde" ] }
|
||||
thiserror = { version = "1.0" }
|
||||
whoami = { version = "1.5" }
|
||||
merge = { version = "=0.2.0" }
|
||||
sysinfo = { version = "=0.34.2", features = [ "serde" ] }
|
||||
thiserror = { version = "2" }
|
||||
whoami = { version = "=1.6.0" }
|
||||
|
||||
# macro
|
||||
paste = { version = "1" }
|
||||
quote = { version = "1" }
|
||||
syn = { version = "2" }
|
||||
proc-macro2 = { version = "1" }
|
||||
|
||||
[patch.crates-io]
|
||||
# tauri-specta = { git = "https://github.com/specta-rs/tauri-specta", rev = "6fe9532a44b8b683b4dacd178b0120ee585bea23" }
|
||||
specta = { git = "https://github.com/specta-rs/specta", rev = "30661263250e7b6f9b36cdc56e6c843ac5abb226" }
|
||||
specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "30661263250e7b6f9b36cdc56e6c843ac5abb226" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
|
|
@ -186,17 +204,6 @@ style = { level = "warn", priority = -1 }
|
|||
suspicious = { level = "warn", priority = -1 }
|
||||
unnecessary_cast = "warn"
|
||||
|
||||
[workspace.lints.rust]
|
||||
deprecated = "warn"
|
||||
deprecated_in_future = "forbid"
|
||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unused_allocation = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_must_use = "deny"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[profile.dev]
|
||||
# add debug symbols/logic and optimize for reduced build time (parallel)
|
||||
codegen-units = 256 # make parallel incremental builds faster
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ authors = { workspace = true }
|
|||
|
||||
[lib]
|
||||
name = "onelauncher_gui"
|
||||
crate-type = [ "staticlib", "cdylib", "lib" ]
|
||||
crate-type = [
|
||||
"staticlib",
|
||||
"cdylib",
|
||||
"lib"
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -22,31 +26,40 @@ workspace = true
|
|||
tauri-build = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
onelauncher = { workspace = true, features = [ "tauri", "specta" ] }
|
||||
onelauncher_macros = { workspace = true }
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
# core
|
||||
onelauncher_core = { workspace = true, features = [ "tauri" ] }
|
||||
interpulse = { workspace = true }
|
||||
|
||||
# tauri
|
||||
tauri-plugin-window-state = { workspace = true }
|
||||
tauri-plugin-single-instance = { workspace = true }
|
||||
tauri-plugin-updater = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
tauri-plugin-dialog = { workspace = true }
|
||||
tauri-plugin-deep-link = { workspace = true }
|
||||
|
||||
# code gen
|
||||
tauri-specta = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
specta-typescript = { workspace = true }
|
||||
interpulse = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
# logging
|
||||
tracing = { workspace = true }
|
||||
tracing-error = { workspace = true }
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
# serialization & text
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
# io
|
||||
tokio = { workspace = true }
|
||||
|
||||
# utils
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dependencies.tauri]
|
||||
workspace = true
|
||||
|
|
@ -58,18 +71,10 @@ features = [
|
|||
"specta"
|
||||
]
|
||||
|
||||
[dev-dependencies.tauri]
|
||||
workspace = true
|
||||
features = [ "test" ]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = { workspace = true }
|
||||
objc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
webkit2gtk = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = [ "custom-protocol", "devtools" ]
|
||||
default = [
|
||||
"custom-protocol",
|
||||
"devtools"
|
||||
]
|
||||
devtools = [ "tauri/devtools" ]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
"core:webview:allow-internal-toggle-devtools",
|
||||
|
||||
"core:event:default",
|
||||
"shell:allow-open",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@
|
|||
"allow": [
|
||||
"$DATA/OneLauncher/**",
|
||||
"$RESOURCE/**"
|
||||
],
|
||||
"deny": ["$DATA/OneLauncher/db/*.stronghold"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"freezePrototype": true,
|
||||
|
|
@ -79,7 +78,7 @@
|
|||
"desktopTemplate": "../../packages/distribution/linux/org.polyfrost.launcher.desktop.template"
|
||||
},
|
||||
"appimage": {
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
"shortDescription": "OneLauncher",
|
||||
|
|
|
|||
|
|
@ -8,4 +8,10 @@ export default defineConfig({
|
|||
rules: {
|
||||
'ts/no-use-before-define': 'off',
|
||||
},
|
||||
toml: {
|
||||
overrides: {
|
||||
'toml/array-element-newline': ['error', 'always'],
|
||||
'toml/no-mixed-type-in-array': ['off'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,14 +14,18 @@ authors = { workspace = true }
|
|||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = [ "cli" ]
|
||||
default = [ "tauri" ]
|
||||
tauri = [
|
||||
"dep:tauri",
|
||||
"dep:tauri-specta",
|
||||
"specta",
|
||||
"gui"
|
||||
]
|
||||
specta = [ "dep:specta", "interpulse/specta", "onelauncher_entity/specta" ]
|
||||
specta = [
|
||||
"dep:specta",
|
||||
"interpulse/specta",
|
||||
"onelauncher_entity/specta"
|
||||
]
|
||||
cli = [ "dep:indicatif" ]
|
||||
gui = []
|
||||
|
||||
|
|
@ -80,7 +84,6 @@ md-5 = { workspace = true }
|
|||
p256 = { workspace = true }
|
||||
|
||||
# io
|
||||
zip = { workspace = true }
|
||||
async_zip = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use onelauncher_core::store::Dirs;
|
||||
use onelauncher_core::store::{Core, CoreOptions, Dirs};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
Core::initialize(CoreOptions::default()).await.expect("couldn't initialize core");
|
||||
let dirs = Dirs::get().await.expect("couldn't initialize dirs");
|
||||
|
||||
println!(" Base dir : {}", dirs.base_dir().display());
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"cargo": "cargo",
|
||||
"binary": "cargo run --bin",
|
||||
"example": "cargo run --example",
|
||||
"test": "cargo test"
|
||||
"test": "cargo test",
|
||||
"lint:rs": "cargo clippy --all-targets"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,10 +108,6 @@ pub async fn create_cluster(
|
|||
|
||||
/// Make a cluster playable (installs all necessary game files and dependencies required to play)
|
||||
pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>) -> LauncherResult<&clusters::Model> {
|
||||
const INGRESS_TOTAL: f64 = 100.0;
|
||||
const INGRESS_TASKS: f64 = 4.0;
|
||||
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
|
||||
|
||||
tracing::debug!("preparing cluster {}", cluster.name);
|
||||
|
||||
// if cluster.stage == ClusterStage::Ready {
|
||||
|
|
@ -119,14 +115,6 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>)
|
|||
// return Ok(cluster);
|
||||
// }
|
||||
|
||||
let ingress_id = init_ingress(
|
||||
IngressType::PrepareCluster {
|
||||
cluster_name: cluster.name.clone(),
|
||||
},
|
||||
"preparing cluster",
|
||||
INGRESS_TOTAL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
dao::update_cluster(cluster, async |mut cluster| {
|
||||
cluster.stage = Set(ClusterStage::Downloading);
|
||||
|
|
@ -134,62 +122,7 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>)
|
|||
})
|
||||
.await?;
|
||||
|
||||
let result: LauncherResult<()> = {
|
||||
// TASK 1
|
||||
ingress_id.set_ingress_message("fetching data").await?;
|
||||
let setting_profile =
|
||||
setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut metadata = state.metadata.write().await;
|
||||
|
||||
let version = metadata
|
||||
.get_vanilla_or_fetch()
|
||||
.await?
|
||||
.versions
|
||||
.iter()
|
||||
.find(|v| v.id == cluster.mc_version)
|
||||
.ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))?
|
||||
.clone();
|
||||
|
||||
drop(metadata);
|
||||
|
||||
let loader_version = metadata::get_loader_version(
|
||||
&cluster.mc_version,
|
||||
cluster.mc_loader,
|
||||
cluster.mc_loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TASK 2
|
||||
let mut version_info = metadata::download_version_info(
|
||||
&version,
|
||||
loader_version.as_ref(),
|
||||
Some(&SubIngress::new(&ingress_id, INGRESS_STEP)),
|
||||
force,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
|
||||
version
|
||||
} else {
|
||||
let Some(ver) = &version_info.java_version else {
|
||||
return Err(ClusterError::MissingJavaVersion.into());
|
||||
};
|
||||
|
||||
java::prepare_java(ver.major_version).await?
|
||||
};
|
||||
|
||||
// TASK 3
|
||||
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).await?;
|
||||
|
||||
// TASK 4
|
||||
run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
if let Err(err) = install_cluster(cluster, force).await {
|
||||
tracing::error!("failed to prepare cluster: {}", err);
|
||||
dao::update_cluster(cluster, async |mut cluster| {
|
||||
cluster.stage = Set(ClusterStage::NotReady);
|
||||
|
|
@ -209,6 +142,74 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>)
|
|||
Ok(cluster)
|
||||
}
|
||||
|
||||
async fn install_cluster(cluster: &clusters::Model, force: Option<bool>) -> LauncherResult<()> {
|
||||
const INGRESS_TOTAL: f64 = 100.0;
|
||||
const INGRESS_TASKS: f64 = 4.0;
|
||||
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
|
||||
|
||||
let ingress_id = init_ingress(
|
||||
IngressType::PrepareCluster {
|
||||
cluster_name: cluster.name.clone(),
|
||||
},
|
||||
"preparing cluster",
|
||||
INGRESS_TOTAL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TASK 1
|
||||
ingress_id.set_ingress_message("fetching data").await?;
|
||||
let setting_profile =
|
||||
setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut metadata = state.metadata.write().await;
|
||||
|
||||
let version = metadata
|
||||
.get_vanilla_or_fetch()
|
||||
.await?
|
||||
.versions
|
||||
.iter()
|
||||
.find(|v| v.id == cluster.mc_version)
|
||||
.ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))?
|
||||
.clone();
|
||||
|
||||
drop(metadata);
|
||||
|
||||
let loader_version = metadata::get_loader_version(
|
||||
&cluster.mc_version,
|
||||
cluster.mc_loader,
|
||||
cluster.mc_loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TASK 2
|
||||
let mut version_info = metadata::download_version_info(
|
||||
&version,
|
||||
loader_version.as_ref(),
|
||||
Some(&SubIngress::new(&ingress_id, INGRESS_STEP)),
|
||||
force,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
|
||||
version
|
||||
} else {
|
||||
let Some(ver) = &version_info.java_version else {
|
||||
return Err(ClusterError::MissingJavaVersion.into());
|
||||
};
|
||||
|
||||
java::prepare_java(ver.major_version).await?
|
||||
};
|
||||
|
||||
// TASK 3
|
||||
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).await?;
|
||||
|
||||
// TASK 4
|
||||
run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run forge processors
|
||||
async fn run_forge_processors(
|
||||
cluster: &clusters::Model,
|
||||
|
|
@ -265,11 +266,10 @@ async fn run_forge_processors(
|
|||
ingress.set_ingress_message("running forge processors").await?;
|
||||
let total_length = processors.len();
|
||||
for (index, processor) in processors.iter().enumerate() {
|
||||
if let Some(sides) = &processor.sides {
|
||||
if !sides.contains(&String::from("client")) {
|
||||
if let Some(sides) = &processor.sides
|
||||
&& !sides.contains(&String::from("client")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut cp = processor.classpath.clone();
|
||||
cp.push(processor.jar.clone());
|
||||
|
|
|
|||
|
|
@ -179,12 +179,11 @@ pub async fn main_class(path: String) -> LauncherResult<Option<String>> {
|
|||
|
||||
for line in buf.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("Main-Class:") {
|
||||
if let Some(class) = line.split(':').nth(1) {
|
||||
if line.starts_with("Main-Class:")
|
||||
&& let Some(class) = line.split(':').nth(1) {
|
||||
class_name = Some(class.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -272,11 +271,10 @@ pub fn classpaths(
|
|||
let mut classpaths = libraries
|
||||
.iter()
|
||||
.filter_map(|lib| {
|
||||
if let Some(rules) = &lib.rules {
|
||||
if !super::rules::validate_rules(rules, java_arch, updated) {
|
||||
if let Some(rules) = &lib.rules
|
||||
&& !super::rules::validate_rules(rules, java_arch, updated) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !lib.include_in_classpath {
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -32,11 +32,10 @@ pub async fn launch_minecraft(
|
|||
prepare_cluster(cluster, force).await?;
|
||||
|
||||
let mut settings = get_global_profile().await;
|
||||
if let Some(name) = &cluster.setting_profile_name {
|
||||
if let Some(profile) = get_profile_by_name(name).await? {
|
||||
if let Some(name) = &cluster.setting_profile_name
|
||||
&& let Some(profile) = get_profile_by_name(name).await? {
|
||||
settings.merge(profile);
|
||||
}
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let dirs = Dirs::get().await?;
|
||||
|
|
|
|||
|
|
@ -285,12 +285,11 @@ pub async fn download_libraries(
|
|||
ingress.set_ingress_message("fetching libraries").await?;
|
||||
|
||||
let requests = stream::iter(libraries.iter().map(|lib| async {
|
||||
if let Some(rules) = &lib.rules {
|
||||
if !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) {
|
||||
if let Some(rules) = &lib.rules
|
||||
&& !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) {
|
||||
tracing::debug!("skipping library {} due to rules", lib.name);
|
||||
return Ok::<(), LauncherError>(());
|
||||
}
|
||||
}
|
||||
|
||||
if !lib.downloadable {
|
||||
tracing::debug!("skipping library {} due to downloadability", lib.name);
|
||||
|
|
@ -309,8 +308,8 @@ pub async fn download_libraries(
|
|||
async {
|
||||
if let Some(interpulse::api::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact), ..
|
||||
}) = lib.downloads {
|
||||
if !artifact.url.is_empty() {
|
||||
}) = lib.downloads
|
||||
&& !artifact.url.is_empty() {
|
||||
http::download(
|
||||
Method::GET,
|
||||
&artifact.url,
|
||||
|
|
@ -322,7 +321,6 @@ pub async fn download_libraries(
|
|||
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
|
||||
return Ok::<_, LauncherError>(());
|
||||
}
|
||||
}
|
||||
|
||||
let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
|
||||
http::download(
|
||||
|
|
|
|||
|
|
@ -75,11 +75,10 @@ pub fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &rule.version {
|
||||
if let Ok(regex) = Regex::new(version.as_str()) {
|
||||
if let Some(version) = &rule.version
|
||||
&& let Ok(regex) = Regex::new(version.as_str()) {
|
||||
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
rule_match
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ use crate::LauncherResult;
|
|||
use crate::store::State;
|
||||
use crate::store::ingress::{IngressId, IngressType, SubIngress};
|
||||
|
||||
#[onelauncher_macro::specta(with_event)]
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct IngressPayload {
|
||||
pub id: Uuid,
|
||||
|
|
|
|||
|
|
@ -122,13 +122,10 @@ pub async fn get_recommended_java(
|
|||
// Settings profile is an override (it has highest priority)
|
||||
if let Some(profile) = profile
|
||||
&& !profile.is_global()
|
||||
{
|
||||
if let Some(java_id) = profile.java_id {
|
||||
if let Ok(Some(java)) = dao::get_java_by_id(java_id).await {
|
||||
&& let Some(java_id) = profile.java_id
|
||||
&& let Ok(Some(java)) = dao::get_java_by_id(java_id).await {
|
||||
return Ok(Some(java));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the version info has a suggested version
|
||||
let Some(supported_ver) = &info.java_version else {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use serde::Serialize;
|
|||
|
||||
use crate::store::processes::Process;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum ProcessPayload {
|
||||
Starting {
|
||||
command: String,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use serde::Serialize;
|
|||
|
||||
use crate::{error::LauncherResult, store::proxy::ProxyState};
|
||||
|
||||
#[onelauncher_macro::specta(with_event)]
|
||||
#[derive(Debug, Serialize)]
|
||||
#[onelauncher_macro::specta(event)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum LauncherEvent {
|
||||
Ingress(crate::api::ingress::IngressPayload),
|
||||
Message(crate::api::proxy::message::MessagePayload),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#[cfg(feature = "tauri")] mod proxy_tauri;
|
||||
#[cfg(feature = "cli")] mod proxy_cli;
|
||||
mod proxy_empty;
|
||||
#[cfg(feature = "tauri")] pub mod proxy_tauri;
|
||||
#[cfg(feature = "cli")] pub mod proxy_cli;
|
||||
pub mod proxy_empty;
|
||||
|
||||
#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri;
|
||||
#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli;
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ impl LauncherProxy for ProxyEmpty {
|
|||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
fn hide_main_window(&self) -> crate::LauncherResult<()> {
|
||||
fn hide_main_window(&self) -> LauncherResult<()> {
|
||||
println!("hidden window");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
fn show_main_window(&self) -> crate::LauncherResult<()> {
|
||||
fn show_main_window(&self) -> LauncherResult<()> {
|
||||
println!("shown window");
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||
use tauri_specta::Event;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy, message::MessageLevel}, LauncherResult};
|
||||
use crate::{api::proxy::{event::LauncherEvent, LauncherProxy, message::MessageLevel}, LauncherResult};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProxyTauri {
|
||||
|
|
@ -21,19 +21,16 @@ impl ProxyTauri {
|
|||
#[async_trait::async_trait]
|
||||
impl LauncherProxy for ProxyTauri {
|
||||
async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> {
|
||||
match event {
|
||||
LauncherEvent::Message(message) => {
|
||||
match message.level {
|
||||
MessageLevel::Info => {},
|
||||
MessageLevel::Warning => warn!("{}", message.message),
|
||||
MessageLevel::Error => error!("{}", message.message),
|
||||
};
|
||||
},
|
||||
_ => {}
|
||||
if let LauncherEvent::Message(message) = &event {
|
||||
match message.level {
|
||||
MessageLevel::Info => {},
|
||||
MessageLevel::Warn => warn!("{}", message.message),
|
||||
MessageLevel::Error => error!("{}", message.message),
|
||||
}
|
||||
}
|
||||
|
||||
self.handle
|
||||
.emit_all(LauncherEvent::NAME, event)
|
||||
.emit(LauncherEvent::NAME, event)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use serde::Serialize;
|
|||
|
||||
use super::event::send_event;
|
||||
|
||||
#[onelauncher_macro::specta(with_event)]
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct MessagePayload {
|
||||
pub level: MessageLevel,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
use onelauncher_entity::setting_profiles;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
use crate::{error::LauncherResult, store::State};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::State;
|
||||
|
||||
pub mod dao {
|
||||
use onelauncher_entity::setting_profiles;
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait};
|
||||
|
||||
use crate::{error::{DaoError, LauncherResult}, store::State};
|
||||
use crate::error::{DaoError, LauncherResult};
|
||||
use crate::store::State;
|
||||
|
||||
pub async fn get_all_profiles() -> LauncherResult<Vec<setting_profiles::Model>> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
Ok(setting_profiles::Entity::find()
|
||||
.all(db)
|
||||
.await?)
|
||||
Ok(setting_profiles::Entity::find().all(db).await?)
|
||||
}
|
||||
|
||||
pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult<setting_profiles::Model> {
|
||||
pub async fn get_profile_or_default(
|
||||
name: Option<&String>,
|
||||
) -> LauncherResult<setting_profiles::Model> {
|
||||
if let Some(name) = name {
|
||||
let profile = get_profile_by_name(name).await?;
|
||||
if let Some(profile) = profile {
|
||||
|
|
@ -31,20 +33,22 @@ pub mod dao {
|
|||
Ok(settings.global_game_settings.clone())
|
||||
}
|
||||
|
||||
pub async fn get_profile_by_name(name: &str) -> LauncherResult<Option<setting_profiles::Model>> {
|
||||
pub async fn get_profile_by_name(
|
||||
name: &str,
|
||||
) -> LauncherResult<Option<setting_profiles::Model>> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
Ok(setting_profiles::Entity::find_by_id(name)
|
||||
.one(db)
|
||||
.await?)
|
||||
Ok(setting_profiles::Entity::find_by_id(name).one(db).await?)
|
||||
}
|
||||
|
||||
pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::Model> {
|
||||
if let Some(name) = profile.name.try_as_ref() {
|
||||
if name.is_empty() || profile.is_global() {
|
||||
return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into());
|
||||
}
|
||||
pub async fn insert_profile(
|
||||
profile: setting_profiles::ActiveModel,
|
||||
) -> LauncherResult<setting_profiles::Model> {
|
||||
if let Some(name) = profile.name.try_as_ref()
|
||||
&& (name.is_empty() || profile.is_global())
|
||||
{
|
||||
return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into());
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
|
|
@ -57,7 +61,9 @@ pub mod dao {
|
|||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?;
|
||||
let deleted = setting_profiles::Entity::delete_by_id(name)
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
if deleted.rows_affected == 0 {
|
||||
return Err(DaoError::NotFound.into());
|
||||
|
|
@ -78,7 +84,9 @@ pub async fn get_global_profile() -> setting_profiles::Model {
|
|||
|
||||
/// Creates a new setting profile and inserts it into the database. Returns the inserted entry
|
||||
pub async fn create_profile<F>(name: &str, block: F) -> LauncherResult<setting_profiles::Model>
|
||||
where F: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel> {
|
||||
where
|
||||
F: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel>,
|
||||
{
|
||||
let model = setting_profiles::ActiveModel {
|
||||
name: Set(name.to_string()),
|
||||
..Default::default()
|
||||
|
|
|
|||
|
|
@ -365,8 +365,8 @@ pub struct SaveDeviceToken {
|
|||
}
|
||||
|
||||
/// Core variables passed throughout the Minecraft login flow.
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct MinecraftLogin {
|
||||
pub verify: String,
|
||||
/// The OAuth challenge generated at the start of the flow.
|
||||
|
|
@ -378,8 +378,8 @@ pub struct MinecraftLogin {
|
|||
}
|
||||
|
||||
/// A structure of all needed Minecraft credentials for logging in and account management.
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct MinecraftCredentials {
|
||||
/// The uuid of the credentials.
|
||||
pub id: Uuid,
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ use uuid::Uuid;
|
|||
|
||||
use crate::api::cluster;
|
||||
use crate::api::cluster::dao::ClusterId;
|
||||
use crate::api::game::log::{censor_line, CensorMap};
|
||||
use crate::api::game::log::{CensorMap, censor_line};
|
||||
use crate::api::processes::ProcessPayload;
|
||||
use crate::api::proxy::event::{send_event, LauncherEvent};
|
||||
use crate::api::proxy::event::{LauncherEvent, send_event};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::send_error;
|
||||
use crate::utils::io::IOError;
|
||||
|
||||
use super::credentials::MinecraftCredentials;
|
||||
use super::State;
|
||||
use super::credentials::MinecraftCredentials;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ProcessError {
|
||||
|
|
@ -37,7 +37,8 @@ pub struct ProcessStore {
|
|||
processes: RwLock<HashMap<u32, Arc<RwLock<Process>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Process {
|
||||
pub pid: u32,
|
||||
pub started_at: DateTime<Utc>,
|
||||
|
|
@ -47,7 +48,7 @@ pub struct Process {
|
|||
#[serde(skip)]
|
||||
pub child_type: Arc<RwLock<ChildType>>,
|
||||
#[serde(skip)]
|
||||
process_loop: Option<JoinHandle<LauncherResult<i32>>>,
|
||||
process_loop: Option<Arc<JoinHandle<LauncherResult<i32>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -112,10 +113,7 @@ impl ProcessStore {
|
|||
}
|
||||
|
||||
/// Returns a list of all running processes for a given cluster.
|
||||
pub async fn get_running_by_cluster(
|
||||
&self,
|
||||
cluster_id: ClusterId,
|
||||
) -> Vec<Arc<RwLock<Process>>> {
|
||||
pub async fn get_running_by_cluster(&self, cluster_id: ClusterId) -> Vec<Arc<RwLock<Process>>> {
|
||||
let mut list = Vec::new();
|
||||
let read_guard = self.processes.read().await;
|
||||
|
||||
|
|
@ -129,10 +127,7 @@ impl ProcessStore {
|
|||
}
|
||||
|
||||
/// Returns a list of all running processes for a given account.
|
||||
pub async fn get_running_by_account(
|
||||
&self,
|
||||
account_id: Uuid,
|
||||
) -> Vec<Arc<RwLock<Process>>> {
|
||||
pub async fn get_running_by_account(&self, account_id: Uuid) -> Vec<Arc<RwLock<Process>>> {
|
||||
let mut list = Vec::new();
|
||||
let read_guard = self.processes.read().await;
|
||||
|
||||
|
|
@ -180,16 +175,18 @@ impl ProcessStore {
|
|||
settings: &setting_profiles::Model,
|
||||
mut command: Command,
|
||||
) -> LauncherResult<Arc<RwLock<Process>>> {
|
||||
send_event(LauncherEvent::Process(ProcessPayload::Starting { command: format!("{command:?}") })).await;
|
||||
send_event(LauncherEvent::Process(ProcessPayload::Starting {
|
||||
command: format!("{command:?}"),
|
||||
}))
|
||||
.await;
|
||||
|
||||
if let Some(pre_hook) = &settings.hook_pre {
|
||||
if let Err(err) = run_hook(pre_hook, cwd.clone()).await {
|
||||
send_error!("{err:?}");
|
||||
}
|
||||
if let Some(pre_hook) = &settings.hook_pre
|
||||
&& let Err(err) = run_hook(pre_hook, cwd.clone()).await
|
||||
{
|
||||
send_error!("{err:?}");
|
||||
}
|
||||
|
||||
command
|
||||
.stdout(Stdio::piped())
|
||||
command.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.stdin(Stdio::null());
|
||||
|
||||
|
|
@ -205,10 +202,13 @@ impl ProcessStore {
|
|||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if settings.os_extra.as_ref().is_some_and(|s| s.enable_gamemode.is_some_and(|x| x)) {
|
||||
if let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid) {
|
||||
tracing::warn!("failed to enable gamemode, continuing: {}", err);
|
||||
}
|
||||
if settings
|
||||
.os_extra
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.enable_gamemode.is_some_and(|x| x))
|
||||
&& let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid)
|
||||
{
|
||||
tracing::warn!("failed to enable gamemode, continuing: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +225,8 @@ impl ProcessStore {
|
|||
send_event(LauncherEvent::Process(ProcessPayload::Output {
|
||||
pid,
|
||||
output: line,
|
||||
})).await;
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -237,9 +238,12 @@ impl ProcessStore {
|
|||
post_hook: settings.hook_post.clone(),
|
||||
account_id: creds.id,
|
||||
child_type: child_type.clone(),
|
||||
process_loop: Some(tokio::task::spawn(Self::process_loop(
|
||||
cluster_id, cwd, settings.hook_post.clone(), child_type,
|
||||
))),
|
||||
process_loop: Some(Arc::new(tokio::task::spawn(Self::process_loop(
|
||||
cluster_id,
|
||||
cwd,
|
||||
settings.hook_post.clone(),
|
||||
child_type,
|
||||
)))),
|
||||
};
|
||||
|
||||
let process = Arc::new(RwLock::new(process));
|
||||
|
|
@ -262,27 +266,8 @@ impl ProcessStore {
|
|||
child: Arc<RwLock<ChildType>>,
|
||||
) -> LauncherResult<i32> {
|
||||
let pid = child.read().await.id();
|
||||
let mut last_updated = Utc::now();
|
||||
|
||||
let exit_code = loop {
|
||||
let mut child = child.write().await;
|
||||
if let Some(status) = child.try_wait()? {
|
||||
tracing::info!("process exited with status: {:?}", status);
|
||||
break status;
|
||||
}
|
||||
|
||||
// Every 60 seconds, update the playtime of the process
|
||||
let duration = Utc::now().signed_duration_since(last_updated).num_seconds();
|
||||
if duration > 60 {
|
||||
last_updated = Utc::now();
|
||||
|
||||
if let Err(err) = cluster::update_playtime(cluster_id, duration).await {
|
||||
tracing::error!("failed to update playtime: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
};
|
||||
let exit_code = wait_for_exit(cluster_id, &child).await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let store = &state.processes;
|
||||
|
|
@ -292,13 +277,17 @@ impl ProcessStore {
|
|||
write_guard.remove(&pid);
|
||||
tracing::debug!("removed process with pid {}", pid);
|
||||
|
||||
if let Some(post_hook) = post_hook {
|
||||
if let Err(err) = run_hook(&post_hook, cwd).await {
|
||||
send_error!("{err:?}");
|
||||
}
|
||||
if let Some(post_hook) = post_hook
|
||||
&& let Err(err) = run_hook(&post_hook, cwd).await
|
||||
{
|
||||
send_error!("{err:?}");
|
||||
}
|
||||
|
||||
send_event(LauncherEvent::Process(ProcessPayload::Stopped { pid, exit_code })).await;
|
||||
send_event(LauncherEvent::Process(ProcessPayload::Stopped {
|
||||
pid,
|
||||
exit_code,
|
||||
}))
|
||||
.await;
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
|
@ -332,4 +321,28 @@ async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult<Option<i32>> {
|
|||
} else {
|
||||
Err(ProcessError::HookUnsuccessful(exit_code).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_exit(cluster_id: ClusterId, child: &Arc<RwLock<ChildType>>) -> LauncherResult<i32> {
|
||||
let mut last_updated = Utc::now();
|
||||
|
||||
loop {
|
||||
let mut child = child.write().await;
|
||||
if let Some(status) = child.try_wait()? {
|
||||
tracing::info!("process exited with status: {:?}", status);
|
||||
return Ok(status);
|
||||
}
|
||||
|
||||
// Every 60 seconds, update the playtime of the process
|
||||
let duration = Utc::now().signed_duration_since(last_updated).num_seconds();
|
||||
if duration > 60 {
|
||||
last_updated = Utc::now();
|
||||
|
||||
if let Err(err) = cluster::update_playtime(cluster_id, duration).await {
|
||||
tracing::error!("failed to update playtime: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ pub enum IOError {
|
|||
path: String,
|
||||
},
|
||||
#[error(transparent)]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
#[error(transparent)]
|
||||
IOError(#[from] std::io::Error),
|
||||
#[error("json deserialization error: {0}")]
|
||||
DeserializeError(#[from] serde_json::Error),
|
||||
|
|
@ -295,11 +293,10 @@ pub async fn unzip_bytes_filtered(
|
|||
read_zip_entries_bytes(data, async |_, entry, entry_reader| {
|
||||
let file_name = entry.filename().as_str()?;
|
||||
|
||||
if let Some(filter) = &filter_entries {
|
||||
if !filter(file_name) {
|
||||
if let Some(filter) = &filter_entries
|
||||
&& !filter(file_name) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let path = dest_path.as_ref().join(sanitize_path(file_name));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
[package]
|
||||
name = "onelauncher"
|
||||
description = "The core of OneLauncher"
|
||||
version = { workspace = true }
|
||||
license = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
readme = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# feature flag allowing us to interact with tauri (for loading animations, errors showing on gui, etc)
|
||||
# also allows us to compile onelauncher core without tauri
|
||||
tauri = [ "dep:tauri", "dep:tauri-specta", "specta" ]
|
||||
specta = [ "dep:specta" ]
|
||||
cli = [ "dep:indicatif" ]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
# features[default]
|
||||
onelauncher_macros = { workspace = true }
|
||||
onelauncher_utils = { workspace = true }
|
||||
interpulse = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-error = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
notify-debouncer-mini = { workspace = true }
|
||||
iota_stronghold = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_ini = { workspace = true }
|
||||
serde_repr = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
sha1_smol = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
murmur2 = { workspace = true }
|
||||
md5 = { workspace = true }
|
||||
async_zip = { workspace = true }
|
||||
discord-rich-presence = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
whoami = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
async-tungstenite = { workspace = true }
|
||||
async-recursion = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
p256 = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
# features[tauri]
|
||||
tauri = { workspace = true, optional = true }
|
||||
tauri-specta = { workspace = true, optional = true }
|
||||
|
||||
# features[specta]
|
||||
specta = { workspace = true, optional = true }
|
||||
|
||||
# features[cli]
|
||||
indicatif = { workspace = true, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
dotenvy = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winreg = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
onelauncher_gamemode = { workspace = true }
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "@onelauncher/core_old",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"description": "Next-generation open source Minecraft launcher core",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:polyfrost/onelauncher",
|
||||
"directory": "packages/core_old"
|
||||
},
|
||||
"bugs": "https://github.com/polyfrost/onelauncher/issues",
|
||||
"sideEffects": false,
|
||||
"scripts": {}
|
||||
}
|
||||
|
|
@ -1,372 +0,0 @@
|
|||
//! `OneLauncher` log management
|
||||
|
||||
use crate::data::{Credentials, Directories, MinecraftCredentials};
|
||||
use crate::prelude::ClusterPath;
|
||||
use crate::utils::http;
|
||||
use futures::TryFutureExt;
|
||||
use onelauncher_utils::io::{self, IOError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||
|
||||
// TODO: put this in the global store
|
||||
/// Core logging state and reader for `OneLauncher`.
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct LogManager {
|
||||
/// Type log type associated with this log file.
|
||||
pub log_type: LogType,
|
||||
/// The age of this log as a [`u64`] in seconds.
|
||||
pub age: u64,
|
||||
/// The log file to read as a [`String`].
|
||||
pub log_file: String,
|
||||
/// The parsed and censored output of the logfile.
|
||||
pub output: Option<LogOutput>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum LogType {
|
||||
Info,
|
||||
Crash,
|
||||
}
|
||||
|
||||
impl LogManager {
|
||||
/// Initialize a new [`LogManager`].
|
||||
async fn initialize(
|
||||
log_type: LogType,
|
||||
age: SystemTime,
|
||||
cluster: &ClusterPath,
|
||||
log_file: String,
|
||||
clear: Option<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
log_type,
|
||||
age: age
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
|
||||
.as_secs(),
|
||||
output: if clear.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(get_output_by_file(cluster, log_type, &log_file).await?)
|
||||
},
|
||||
log_file,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify all [`LogManager`]s of a certain [`LogType`]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_by_type(
|
||||
cluster_path: &ClusterPath,
|
||||
log_type: LogType,
|
||||
clear: Option<bool>,
|
||||
logs: &mut Vec<crate::Result<LogManager>>,
|
||||
) -> crate::Result<()> {
|
||||
let logs_folder = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(cluster_path).await?,
|
||||
LogType::Crash => Directories::crash_reports_dir(cluster_path).await?,
|
||||
};
|
||||
|
||||
if logs_folder.exists() {
|
||||
for entry in
|
||||
std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
let entry: std::fs::DirEntry =
|
||||
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let age = entry
|
||||
.metadata()?
|
||||
.created()
|
||||
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = path.file_name() {
|
||||
let name = name.to_string_lossy().to_string();
|
||||
|
||||
logs.push(LogManager::initialize(log_type, age, cluster_path, name, clear).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all [`LogManager`]s in the global [`State`].
|
||||
pub async fn get_logs(
|
||||
cluster_path: &ClusterPath,
|
||||
clear: Option<bool>,
|
||||
) -> crate::Result<Vec<LogManager>> {
|
||||
let dir = Directories::cluster_logs_dir(cluster_path).await?;
|
||||
if !dir.exists() {
|
||||
io::create_dir(dir).await?;
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut logs = Vec::new();
|
||||
get_logs_by_type(cluster_path, LogType::Info, clear, &mut logs).await?;
|
||||
get_logs_by_type(cluster_path, LogType::Crash, clear, &mut logs).await?;
|
||||
|
||||
let mut logs = logs
|
||||
.into_iter()
|
||||
.collect::<crate::Result<Vec<LogManager>>>()?;
|
||||
logs.sort_by(|a, b| match (a.log_file.as_str(), b.log_file.as_str()) {
|
||||
("latest.log", _) => std::cmp::Ordering::Less,
|
||||
(_, "latest.log") => std::cmp::Ordering::Greater,
|
||||
_ => b.age.cmp(&a.age).then(b.log_file.cmp(&a.log_file)),
|
||||
});
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
/// Delete all stored logs from a specific [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(cluster_path: ClusterPath) -> crate::Result<()> {
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
|
||||
for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? {
|
||||
let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the [`LogManager`] for a specific [`ClusterPath`] log file.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_by_file(
|
||||
cluster_path: ClusterPath,
|
||||
log_type: LogType,
|
||||
log_file: String,
|
||||
) -> crate::Result<LogManager> {
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
|
||||
let path = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(&cluster_path).await,
|
||||
LogType::Crash => Directories::crash_reports_dir(&cluster_path).await,
|
||||
}?
|
||||
.join(&log_file);
|
||||
|
||||
let metadata = std::fs::metadata(&path)?;
|
||||
let age = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
|
||||
LogManager::initialize(log_type, age, &cluster_path, log_file, Some(true)).await
|
||||
}
|
||||
|
||||
/// Get the default [`LogCursor`] for a [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_log_cursor(cluster_path: ClusterPath, cursor: u64) -> crate::Result<LogCursor> {
|
||||
get_live_log_cursor(cluster_path, "latest.log", cursor).await
|
||||
}
|
||||
|
||||
/// Get a live [`LogCursor`] for a [`ClusterPath`]'s log file.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_live_log_cursor(
|
||||
cluster_path: ClusterPath,
|
||||
log_file: &str,
|
||||
mut cursor: u64,
|
||||
) -> crate::Result<LogCursor> {
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
|
||||
let path = logs_folder.join(log_file);
|
||||
if !path.exists() {
|
||||
return Ok(LogCursor {
|
||||
cursor: 0,
|
||||
new: false,
|
||||
output: LogOutput(String::new()),
|
||||
});
|
||||
}
|
||||
|
||||
let mut file = tokio::fs::File::open(&path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
|
||||
let new = if cursor > metadata.len() {
|
||||
cursor = 0;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
file.seek(std::io::SeekFrom::Start(cursor))
|
||||
.map_err(|e| IOError::with_path(e, &path))
|
||||
.await?;
|
||||
let bytes_read = file
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| IOError::with_path(e, &path))
|
||||
.await?;
|
||||
let output = String::from_utf8_lossy(&buf).to_string();
|
||||
let cursor = cursor + bytes_read as u64;
|
||||
let creds: Vec<MinecraftCredentials> = crate::State::get()
|
||||
.await?
|
||||
.users
|
||||
.read()
|
||||
.await
|
||||
.users
|
||||
.clone()
|
||||
.into_values()
|
||||
.collect();
|
||||
let output = LogOutput::censor_secrets(output, &creds, &None);
|
||||
Ok(LogCursor {
|
||||
cursor,
|
||||
output,
|
||||
new,
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a specific minecraft log file.
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs_by_file(
|
||||
cluster_path: ClusterPath,
|
||||
log_type: LogType,
|
||||
log_file: &str,
|
||||
) -> crate::Result<()> {
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
let logs_folder = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(&cluster_path).await?,
|
||||
LogType::Crash => Directories::crash_reports_dir(&cluster_path).await?,
|
||||
};
|
||||
|
||||
let path = logs_folder.join(log_file);
|
||||
io::remove_file(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_log_to_string(path: &std::path::PathBuf) -> crate::Result<String> {
|
||||
if let Some(ext) = path.extension() {
|
||||
let mut result = String::new();
|
||||
|
||||
if ext == "gz" {
|
||||
result = io::read_gz_to_string(path).await?;
|
||||
} else if ext == "log" || ext == "txt" {
|
||||
// On Java 17 and older, UTF-8 is not the default charset (https://openjdk.org/jeps/400)
|
||||
// Minecraft on Windows on older versions sometimes likes to output the log
|
||||
// in an encoding other than UTF-8, which would make OneLauncher throw an error
|
||||
// when attempting to read it into a String (Rust Strings are UTF-8)
|
||||
// TODO: Potentially explore a better solution for non UTF-8 log reading?
|
||||
let bytes = io::read(path).await?;
|
||||
result = String::from_utf8_lossy(&bytes).to_string();
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("log file extension {} not supported", path.display()).into())
|
||||
}
|
||||
|
||||
/// Get a specific [`ClusterPath`] log file's [`LogOutput`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_file(
|
||||
cluster_path: &ClusterPath,
|
||||
log_type: LogType,
|
||||
log_file: &str,
|
||||
) -> crate::Result<LogOutput> {
|
||||
let logs_folder = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(cluster_path).await?,
|
||||
LogType::Crash => Directories::crash_reports_dir(cluster_path).await?,
|
||||
};
|
||||
let path = logs_folder.join(log_file);
|
||||
let credentials: Vec<MinecraftCredentials> = crate::State::get()
|
||||
.await?
|
||||
.users
|
||||
.read()
|
||||
.await
|
||||
.users
|
||||
.clone()
|
||||
.into_values()
|
||||
.collect();
|
||||
|
||||
let result = read_log_to_string(&path).await?;
|
||||
|
||||
return Ok(LogOutput::censor_secrets(result, &credentials, &None));
|
||||
}
|
||||
|
||||
/// The log cursor used to parse logs passed into [`LogManager`].
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct LogCursor {
|
||||
/// The cursor ID.
|
||||
pub cursor: u64,
|
||||
/// The [`LogOutput`] associated with this cursor.
|
||||
pub output: LogOutput,
|
||||
/// Whether or not this corresponds is a new log file.
|
||||
pub new: bool,
|
||||
}
|
||||
|
||||
/// The log output, a wrapper around [`String`], with utilities for parsing and censoring log contents.
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(transparent)]
|
||||
pub struct LogOutput(pub String);
|
||||
|
||||
impl LogOutput {
|
||||
/// Censor user secrets because sometimes mclogs misses them, including username, realname and minecraft credentials.
|
||||
pub fn censor_secrets(
|
||||
mut output: String,
|
||||
credentials: &Vec<MinecraftCredentials>,
|
||||
_credentials_store: &Option<Credentials>,
|
||||
) -> Self {
|
||||
let username = whoami::username();
|
||||
let realname = whoami::realname();
|
||||
output = output.replace(&format!("/{username}/"), "/{ENV_USERNAME}/");
|
||||
output = output.replace(&format!("\\{username}\\"), "\\{ENV_USERNAME}\\");
|
||||
output = output.replace(&format!("/{realname}/"), "/{ENV_REALNAME}/");
|
||||
output = output.replace(&format!("\\{realname}\\"), "\\{ENV_REALNAME}\\");
|
||||
|
||||
for cred in credentials {
|
||||
output = output.replace(&cred.access_token, "{MC_ACCESS_TOKEN}");
|
||||
output = output.replace(&cred.username, "{MC_USERNAME}");
|
||||
output = output.replace(&cred.id.as_simple().to_string(), "{MC_UUID}");
|
||||
output = output.replace(&cred.id.as_hyphenated().to_string(), "{MC_UUID}");
|
||||
}
|
||||
|
||||
Self(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct McLogsResponse {
|
||||
pub success: bool,
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Upload a log to https://mclo.gs/. If successful, it returns the log id
|
||||
#[tracing::instrument]
|
||||
pub async fn upload_log(path: &ClusterPath, log: LogOutput) -> crate::Result<String> {
|
||||
let log = log.0;
|
||||
let mut form = HashMap::new();
|
||||
form.insert("content", log);
|
||||
|
||||
let parsed = serde_json::from_slice::<McLogsResponse>(
|
||||
&http::REQWEST_CLIENT
|
||||
.post(format!("{}/log", crate::constants::MCLOGS_API_URL))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.form(&form)
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
if parsed.success {
|
||||
parsed.id.map_or_else(
|
||||
|| Err(anyhow::anyhow!("failed to get log id from mclo.gs").into()),
|
||||
Ok,
|
||||
)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"failed to upload log to mclo.gs: {}",
|
||||
parsed.error.unwrap_or_default()
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
pub mod logger;
|
||||
pub mod package;
|
||||
pub mod screenshots;
|
||||
pub mod worlds;
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
#![allow(clippy::significant_drop_tightening)]
|
||||
|
||||
use crate::data::{Loader, ManagedPackage, ManagedVersion, PackageType};
|
||||
use crate::prelude::PackagePath;
|
||||
use crate::processor::Cluster;
|
||||
use crate::proxy::send::{init_ingress, send_ingress, send_internet};
|
||||
use crate::store::{ClusterPath, ManagedVersionFile, Package, PackageMetadata};
|
||||
use crate::utils::http;
|
||||
use crate::{Result, State};
|
||||
use onelauncher_utils::io;
|
||||
use reqwest::Method;
|
||||
// TODO: Implement proper error handling
|
||||
|
||||
/// Find a managed version using filters
|
||||
/// - Game Version (Default: Cluster's MC Version)
|
||||
/// - Loader (Default: Cluster's chosen Loader)
|
||||
/// - Package Version (Default: Latest version available)
|
||||
#[tracing::instrument]
|
||||
pub async fn find_managed_version(
|
||||
package: &ManagedPackage,
|
||||
game_version: Option<String>,
|
||||
loader: Option<Loader>,
|
||||
package_version: Option<String>,
|
||||
) -> Result<ManagedVersion> {
|
||||
let provider = package.provider.clone();
|
||||
|
||||
let versions = provider
|
||||
.get_all_versions(
|
||||
&package.id,
|
||||
game_version.as_ref().map(|v| vec![v.to_owned()]).clone(),
|
||||
loader.map(|l| vec![l]).clone(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(versions.0
|
||||
.iter()
|
||||
.find(|v| {
|
||||
if let Some(package_version) = package_version.as_ref() {
|
||||
return v.id == *package_version;
|
||||
}
|
||||
|
||||
let check_game_version = game_version
|
||||
.as_ref()
|
||||
.is_some_and(|gv| v.game_versions.iter().any(|gv2| *gv2 == *gv));
|
||||
|
||||
let check_loader = loader
|
||||
.as_ref()
|
||||
.is_some_and(|loader| v.loaders.iter().any(|l| *l == *loader));
|
||||
|
||||
check_game_version && check_loader
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("no matching version found"))
|
||||
.cloned()?)
|
||||
}
|
||||
|
||||
/// Download a package to a cluster. Supports filtering by:
|
||||
/// - Game Version (Default: Cluster's MC Version)
|
||||
/// - Loader (Default: Cluster's chosen Loader)
|
||||
/// - Package Version (Default: Latest version available)
|
||||
#[tracing::instrument(skip(package, cluster))]
|
||||
pub async fn download_package(
|
||||
package: &ManagedPackage,
|
||||
cluster: &mut Cluster,
|
||||
game_version: Option<String>,
|
||||
loader: Option<Loader>,
|
||||
package_version: Option<String>,
|
||||
) -> Result<(PackagePath, Package)> {
|
||||
tracing::info!(
|
||||
"preparing package '{}' for cluster '{}'",
|
||||
package.title,
|
||||
cluster.meta.name
|
||||
);
|
||||
send_internet(crate::proxy::InternetPayload::InstallPackage {
|
||||
id: package.id.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let loader = loader.unwrap_or(cluster.meta.loader);
|
||||
let game_version = game_version.unwrap_or_else(|| cluster.meta.mc_version.clone());
|
||||
|
||||
let managed_version =
|
||||
find_managed_version(package, Some(game_version), Some(loader), package_version).await?;
|
||||
|
||||
let file = managed_version
|
||||
.get_primary_file()
|
||||
.ok_or_else(|| anyhow::anyhow!("no primary file found"))?;
|
||||
|
||||
let package_path = download_file(package, &managed_version, file, &package.package_type, cluster).await?;
|
||||
let sha1 = file.hashes.get("sha1").unwrap_or(&String::new()).clone();
|
||||
|
||||
let package = Package {
|
||||
file_name: file.file_name.clone(),
|
||||
sha1,
|
||||
meta: PackageMetadata::from_managed_package(package.clone(), managed_version),
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
Ok((package_path, package))
|
||||
}
|
||||
|
||||
/// Download a file to a cluster from a managed version file.
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn download_file(
|
||||
package: &ManagedPackage,
|
||||
version: &ManagedVersion,
|
||||
file: &ManagedVersionFile,
|
||||
package_type: &PackageType,
|
||||
cluster: &Cluster,
|
||||
) -> Result<PackagePath> {
|
||||
// TODO: Implement hash checking
|
||||
let cluster_path = &cluster.get_full_path().await?;
|
||||
|
||||
let path = &cluster_path
|
||||
.join(package_type.get_folder())
|
||||
.join(&file.file_name);
|
||||
|
||||
let ingress_id = init_ingress(
|
||||
crate::IngressType::DownloadPackage {
|
||||
cluster_path: cluster_path.to_owned(),
|
||||
package_name: package.title.clone(),
|
||||
icon: None,
|
||||
package_id: Some(package.id.clone()),
|
||||
package_version: Some(version.id.clone()),
|
||||
}, 100.0, "downloading package").await?;
|
||||
|
||||
tracing::info!(
|
||||
"downloading file '{}' version '{}'",
|
||||
file.file_name,
|
||||
version.name
|
||||
);
|
||||
|
||||
let state = State::get().await?;
|
||||
let bytes = http::fetch_advanced(
|
||||
Method::GET,
|
||||
&file.url,
|
||||
file.hashes.get("sha1").map(String::as_str),
|
||||
None,
|
||||
None,
|
||||
Some((&ingress_id, 90.0)),
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Err(err) = send_ingress(&ingress_id, 5.0, Some("saving file")).await {
|
||||
tracing::error!("{err}");
|
||||
}
|
||||
|
||||
if let Err(err) = http::write(&path, &bytes, &state.io_semaphore).await {
|
||||
tracing::error!("failed to write file to cluster: {err}");
|
||||
if path.exists() {
|
||||
let _ = io::remove_file(&path).await;
|
||||
}
|
||||
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
if let Err(err) = send_ingress(&ingress_id, 100.0, Some(format!("downloaded package {}", package.title).as_str())).await {
|
||||
tracing::error!("{err}");
|
||||
}
|
||||
|
||||
drop(state);
|
||||
|
||||
Ok(PackagePath::from_path(path).await?)
|
||||
}
|
||||
|
||||
/// Add a package to a cluster.
|
||||
#[tracing::instrument]
|
||||
pub async fn add_package(
|
||||
cluster_path: &ClusterPath,
|
||||
package_path: PackagePath,
|
||||
package: Package,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut manager = state.packages.write().await;
|
||||
let manager = manager
|
||||
.get_mut(cluster_path)
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
|
||||
|
||||
manager
|
||||
.add_package(package_path, package)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a package from a cluster.
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_package(
|
||||
cluster_path: &ClusterPath,
|
||||
package_path: &PackagePath,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut manager = state.packages.write().await;
|
||||
let manager = manager
|
||||
.get_mut(cluster_path)
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
|
||||
|
||||
manager.remove_package(package_path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn set_package_enabled(
|
||||
cluster_path: &ClusterPath,
|
||||
package_path: &PackagePath,
|
||||
enabled: bool,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut manager = state.packages.write().await;
|
||||
let manager = manager
|
||||
.get_mut(cluster_path)
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
|
||||
|
||||
manager.set_package_enabled(package_path, enabled).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a package from a cluster.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_package(
|
||||
cluster_path: &ClusterPath,
|
||||
package_path: &PackagePath,
|
||||
package_type: PackageType,
|
||||
) -> Result<Package> {
|
||||
let state = State::get().await?;
|
||||
let mut store = state.packages.write().await;
|
||||
let manager = store
|
||||
.get_mut(cluster_path)
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
|
||||
|
||||
Ok(manager
|
||||
.get(package_type)
|
||||
.await
|
||||
.packages
|
||||
.get(package_path)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("package not found"))?)
|
||||
}
|
||||
|
||||
/// Get packages from a cluster.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_packages(
|
||||
cluster_path: &ClusterPath,
|
||||
package_type: PackageType,
|
||||
) -> Result<Vec<Package>> {
|
||||
let state = State::get().await?;
|
||||
let mut store = state.packages.write().await;
|
||||
let manager = store
|
||||
.get_mut(cluster_path)
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
|
||||
|
||||
Ok(manager
|
||||
.get(package_type)
|
||||
.await
|
||||
.packages
|
||||
.values()
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Sync packages from a cluster.
|
||||
#[tracing::instrument]
|
||||
pub async fn sync_packages(cluster_path: &ClusterPath) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut manager = state.packages.write().await;
|
||||
let manager = manager
|
||||
.get_mut(cluster_path)
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
|
||||
|
||||
manager.sync_packages(&state.directories).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync packages from a cluster.
|
||||
#[tracing::instrument]
|
||||
pub async fn sync_packages_by_type(
|
||||
cluster_path: &ClusterPath,
|
||||
package_type: PackageType,
|
||||
clear: Option<bool>
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut manager = state.packages.write().await;
|
||||
let manager = manager
|
||||
.get_mut(cluster_path)
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
|
||||
|
||||
manager
|
||||
.sync_packages_by_type(&state.directories, package_type, clear)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
use crate::Result;
|
||||
use onelauncher_utils::io;
|
||||
|
||||
use crate::store::ClusterPath;
|
||||
|
||||
/// Gets a list of screenshot file names from the [`ClusterPath`]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_screenshots(cluster: &ClusterPath) -> Result<Vec<String>> {
|
||||
let dir = cluster.full_path().await?.join("screenshots");
|
||||
|
||||
if !dir.exists() {
|
||||
io::create_dir(dir).await?;
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut list = vec![];
|
||||
let mut files = io::read_dir(dir).await?;
|
||||
while let Some(file) = files.next_entry().await? {
|
||||
list.push(file.file_name().to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
use crate::Result;
|
||||
use onelauncher_utils::io;
|
||||
|
||||
use crate::store::ClusterPath;
|
||||
|
||||
/// Gets a list of screenshot file names from the [`ClusterPath`]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_worlds(cluster: &ClusterPath) -> Result<Vec<String>> {
|
||||
let dir = cluster.full_path().await?.join("saves");
|
||||
|
||||
if !dir.exists() {
|
||||
io::create_dir(dir).await?;
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut list = vec![];
|
||||
let mut files = io::read_dir(dir).await?;
|
||||
while let Some(file) = files.next_entry().await? {
|
||||
list.push(file.file_name().to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
|
@ -1,333 +0,0 @@
|
|||
//! Utilities for creating Clusters
|
||||
|
||||
use crate::data::Loader;
|
||||
use crate::package::from::CreatePackCluster;
|
||||
use crate::proxy::send::send_cluster;
|
||||
use crate::proxy::ClusterPayloadType;
|
||||
pub use crate::store::{Cluster, ClusterPath, JavaOptions, PackageData, State};
|
||||
use crate::{cluster, package};
|
||||
use interpulse::api::modded::LoaderVersion;
|
||||
use onelauncher_utils::io::{self, canonicalize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Creates an unfinished [`Cluster`] used for building onto it and adds it to the memory [`State`].
|
||||
/// Returns a relative filepath ([`ClusterPath`]) which can be used to access the cluster.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_unfinished_cluster(
|
||||
mut name: String,
|
||||
) -> crate::Result<ClusterPath> {
|
||||
name = cluster::sanitize_cluster_name(&name);
|
||||
tracing::trace!("creating new cluster {}", name);
|
||||
let state = State::get().await?;
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
let mut path = state.directories.clusters_dir().await.join(&name);
|
||||
|
||||
// i hate it here
|
||||
if path.exists() {
|
||||
let mut new_name;
|
||||
let mut new_path;
|
||||
let mut which = 1;
|
||||
loop {
|
||||
new_name = format!("{name} ({which})");
|
||||
new_path = state.directories.clusters_dir().await.join(&new_name);
|
||||
if !new_path.exists() {
|
||||
break;
|
||||
}
|
||||
which += 1;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"collision while creating new cluster: {}, renaming to {}",
|
||||
path.display(),
|
||||
new_path.display()
|
||||
);
|
||||
path = new_path;
|
||||
name = new_name;
|
||||
}
|
||||
|
||||
io::create_dir_all(&path).await?;
|
||||
tracing::info!(
|
||||
"creating cluster at path {}",
|
||||
&canonicalize(&path)?.display()
|
||||
);
|
||||
|
||||
let cluster = Cluster::new(uuid, name, String::from("none")).await?;
|
||||
let result = async {
|
||||
{
|
||||
state
|
||||
.clusters
|
||||
.write()
|
||||
.await
|
||||
.insert(cluster.clone(), false)
|
||||
.await?;
|
||||
}
|
||||
|
||||
State::sync().await?;
|
||||
let state = State::get().await?;
|
||||
let mut packages = state.packages.write().await;
|
||||
packages.add_cluster(cluster.cluster_path());
|
||||
|
||||
Ok(cluster.cluster_path())
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(cluster) => Ok(cluster),
|
||||
Err(err) => {
|
||||
let _ = cluster::remove(&cluster.cluster_path()).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Cluster`] and adds it to the memory [`State`].
|
||||
/// Returns a relative filepath ([`ClusterPath`]) which can be used to access the cluster.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_cluster(
|
||||
name: String,
|
||||
mc_version: String,
|
||||
mod_loader: Loader,
|
||||
loader_version: Option<String>,
|
||||
icon: Option<PathBuf>,
|
||||
icon_url: Option<String>,
|
||||
package_data: Option<PackageData>,
|
||||
skip: Option<bool>,
|
||||
skip_watch: Option<bool>,
|
||||
) -> crate::Result<ClusterPath> {
|
||||
let mut cluster = cluster::get(&create_unfinished_cluster(name).await?)
|
||||
.await?
|
||||
.ok_or(anyhow::anyhow!("failed to get cluster"))?;
|
||||
|
||||
let state = State::get().await?;
|
||||
|
||||
let loader = if mod_loader == Loader::Vanilla {
|
||||
None
|
||||
} else {
|
||||
get_loader_version(mc_version.clone(), mod_loader, loader_version).await?
|
||||
};
|
||||
|
||||
let result = async {
|
||||
cluster.meta.mc_version = mc_version;
|
||||
cluster.meta.loader = mod_loader;
|
||||
cluster.meta.loader_version = loader.clone();
|
||||
|
||||
if let Some(ref icon) = icon {
|
||||
let bytes = io::read(state.directories.caches_dir().await.join(icon)).await?;
|
||||
cluster
|
||||
.set_icon_bytes(
|
||||
&state.directories.caches_dir().await,
|
||||
&state.io_semaphore,
|
||||
bytes::Bytes::from(bytes),
|
||||
&icon.to_string_lossy(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
cluster.meta.icon_url = icon_url;
|
||||
if let Some(loader_version) = loader {
|
||||
cluster.meta.loader = mod_loader;
|
||||
cluster.meta.loader_version = Some(loader_version);
|
||||
}
|
||||
|
||||
cluster.meta.package_data = package_data;
|
||||
if let Some(package_data) = &mut cluster.meta.package_data {
|
||||
package_data.locked =
|
||||
Some(package_data.package_id.is_some() && package_data.version_id.is_some());
|
||||
}
|
||||
|
||||
send_cluster(
|
||||
cluster.uuid,
|
||||
&cluster.cluster_path(),
|
||||
&cluster.meta.name,
|
||||
ClusterPayloadType::Created,
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
state
|
||||
.clusters
|
||||
.write()
|
||||
.await
|
||||
.insert(cluster.clone(), skip_watch.unwrap_or_default())
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !skip.unwrap_or(false) {
|
||||
// let cluster = cluster.clone();
|
||||
// tokio::task::spawn(async move {
|
||||
if let Err(err) = crate::game::install_minecraft(&cluster, None, false).await {
|
||||
tracing::error!(
|
||||
"failed to install Minecraft while creating cluster {}: {}",
|
||||
cluster.uuid,
|
||||
err
|
||||
);
|
||||
}
|
||||
// });
|
||||
}
|
||||
|
||||
State::sync().await?;
|
||||
let state = State::get().await?;
|
||||
let mut packages = state.packages.write().await;
|
||||
packages.add_cluster(cluster.cluster_path());
|
||||
|
||||
Ok(cluster.cluster_path())
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(cluster) => Ok(cluster),
|
||||
Err(err) => {
|
||||
let _ = cluster::remove(&cluster.cluster_path()).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`Cluster`] from a [`CreatePackCluster`] cluster, returning a [`ClusterPath`].
|
||||
pub async fn create_cluster_from_package(cluster: CreatePackCluster) -> crate::Result<ClusterPath> {
|
||||
create_cluster(
|
||||
cluster.name,
|
||||
cluster.mc_version,
|
||||
cluster.mod_loader,
|
||||
cluster.loader_version,
|
||||
cluster.icon,
|
||||
cluster.icon_url,
|
||||
cluster.package_data,
|
||||
cluster.skip,
|
||||
cluster.skip_watch,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a duplicate [`Cluster`] from another [`ClusterPath`], returning a new [`ClusterPath`]
|
||||
pub async fn create_cluster_from_duplicate(from: ClusterPath) -> crate::Result<ClusterPath> {
|
||||
let cluster = cluster::get(&from).await?.ok_or_else(|| {
|
||||
anyhow::anyhow!("failed to get unmanaged cluster from {}", from.to_string())
|
||||
})?;
|
||||
let cluster_path = create_cluster(
|
||||
cluster.meta.name.clone(),
|
||||
cluster.meta.mc_version.clone(),
|
||||
cluster.meta.loader,
|
||||
cluster.meta.loader_version.clone().map(|it| it.id),
|
||||
cluster.meta.icon.clone(),
|
||||
cluster.meta.icon_url.clone(),
|
||||
cluster.meta.package_data.clone(),
|
||||
Some(true),
|
||||
Some(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let copied = package::import::copy_minecraft(
|
||||
cluster_path.clone(),
|
||||
from.full_path().await?,
|
||||
&State::get().await?.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let duplicated = cluster::get(&cluster_path).await?.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"failed to get unmanaged cluster from {}",
|
||||
cluster_path.to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
crate::game::install_minecraft(&duplicated, Some(copied), false).await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut watcher = state.watcher.write().await;
|
||||
Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?;
|
||||
}
|
||||
|
||||
send_cluster(
|
||||
cluster.uuid,
|
||||
&cluster.cluster_path(),
|
||||
&cluster.meta.name,
|
||||
ClusterPayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
|
||||
State::sync().await?;
|
||||
Ok(cluster_path)
|
||||
}
|
||||
|
||||
/// Get the latest [`LoaderVersion`] from a [`Loader`].
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub(crate) async fn get_loader_version(
|
||||
mc_version: String,
|
||||
loader: Loader,
|
||||
loader_version: Option<String>,
|
||||
) -> crate::Result<Option<LoaderVersion>> {
|
||||
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!("{mc_version}-{id}") == it.id
|
||||
|| format!("{mc_version}-{id}-{mc_version}") == it.id
|
||||
}
|
||||
};
|
||||
|
||||
let loader_meta = match loader {
|
||||
Loader::Forge => &metadata.forge,
|
||||
Loader::Fabric => &metadata.fabric,
|
||||
Loader::Quilt => &metadata.quilt,
|
||||
Loader::NeoForge => &metadata.neoforge,
|
||||
// Loader::LegacyFabric => &metadata.legacy_fabric,
|
||||
_ => return Err(CreateClusterError::MissingManifest(loader.to_string()).into()),
|
||||
};
|
||||
|
||||
let loader_meta = loader_meta
|
||||
.to_owned()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get game versions"))?;
|
||||
|
||||
let loaders = &loader_meta
|
||||
.game_versions
|
||||
.iter()
|
||||
.find(|it| {
|
||||
it.id
|
||||
.replace(interpulse::api::modded::DUMMY_REPLACE_STRING, &mc_version)
|
||||
== mc_version
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
CreateClusterError::UnsupportedLoader(loader.to_string(), mc_version.clone())
|
||||
})?
|
||||
.loaders;
|
||||
|
||||
let loader_version = loaders
|
||||
.iter()
|
||||
.find(|&it| filter(it))
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
if version == "stable" {
|
||||
loaders.iter().next().cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| CreateClusterError::InvalidLoaderVersion(version, loader.to_string()))?;
|
||||
|
||||
Ok(Some(loader_version))
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum CreateClusterError {
|
||||
#[error("Loader {0} is unsupported on Minecraft version {1}")]
|
||||
UnsupportedLoader(String, String),
|
||||
#[error("Invalid Loader version {0} for Loader {1}")]
|
||||
InvalidLoaderVersion(String, String),
|
||||
#[error("Failed to get Loader manifest for {0}.")]
|
||||
MissingManifest(String),
|
||||
#[error("Failed to handle I/O operations: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
||||
|
|
@ -1,436 +0,0 @@
|
|||
//! **`OneLauncher` Cluster**
|
||||
//!
|
||||
//! API for creating our managed Minecraft instances, Clusters.
|
||||
|
||||
// TODO: (pauline) fully implement the cluster::self APIs.
|
||||
|
||||
use crate::proxy::send::send_cluster;
|
||||
|
||||
use crate::prelude::{ClusterPath, JavaVersion, PackagePath};
|
||||
use crate::proxy::ClusterPayloadType;
|
||||
pub use crate::store::{Cluster, JavaOptions, State};
|
||||
use crate::store::{MinecraftCredentials, ProcessorChild};
|
||||
|
||||
use onelauncher_utils::io::{self, IOError};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub mod content;
|
||||
pub mod create;
|
||||
pub mod update;
|
||||
|
||||
/// get a cluster by its specified [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get(path: &ClusterPath) -> crate::Result<Option<Cluster>> {
|
||||
let state = State::get().await?;
|
||||
let clusters = state.clusters.read().await;
|
||||
let cluster = clusters.0.get(path).cloned();
|
||||
|
||||
Ok(cluster)
|
||||
}
|
||||
|
||||
/// get a cluster by its specified [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_by_name(name: &str) -> crate::Result<Option<Cluster>> {
|
||||
let state = State::get().await?;
|
||||
let clusters = state.clusters.read().await;
|
||||
let cluster = clusters.0.values().find(|c| c.meta.name == name).cloned();
|
||||
|
||||
Ok(cluster)
|
||||
}
|
||||
|
||||
/// get a list of all [`Cluster`]s
|
||||
#[tracing::instrument]
|
||||
pub async fn list() -> crate::Result<Vec<Cluster>> {
|
||||
let state = State::get().await?;
|
||||
let clusters = state.clusters.read().await;
|
||||
Ok(clusters.0.clone().into_iter().map(|it| it.1).collect())
|
||||
}
|
||||
|
||||
/// get a Map of [`Cluster`] group names containing lists of [`Cluster`]s
|
||||
#[tracing::instrument]
|
||||
pub async fn list_grouped() -> crate::Result<HashMap<String, Vec<Cluster>>> {
|
||||
// TODO: This can 100% be made better
|
||||
let list = list().await?;
|
||||
let mut map = HashMap::<String, Vec<Cluster>>::new();
|
||||
|
||||
for cluster in list {
|
||||
let group = cluster
|
||||
.meta
|
||||
.group
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("Ungrouped"));
|
||||
if let Some(items) = map.get_mut(&group) {
|
||||
items.push(cluster);
|
||||
} else {
|
||||
map.insert(group, vec![cluster]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// run a Minecraft [`Cluster`] using the default credentials.
|
||||
#[tracing::instrument]
|
||||
pub async fn run_default(path: &ClusterPath) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
|
||||
let state = State::get().await?;
|
||||
let default = {
|
||||
let mut users = state.users.write().await;
|
||||
users
|
||||
.get_default()
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("no default credentials found!"))?
|
||||
};
|
||||
|
||||
run_credentials(path, &default).await
|
||||
}
|
||||
|
||||
/// run a Minecraft [`Cluster`] using [`MinecraftCredentials`] for authentication.
|
||||
/// returns an [`Arc`] pointer to [`RwLock`] to [`ProcessorChild`].
|
||||
#[tracing::instrument(skip(creds))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn run(
|
||||
path: &ClusterPath,
|
||||
creds: Option<&MinecraftCredentials>,
|
||||
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
|
||||
if let Some(creds) = creds {
|
||||
run_credentials(path, creds).await
|
||||
} else {
|
||||
run_default(path).await
|
||||
}
|
||||
}
|
||||
|
||||
/// run a Minecraft [`Cluster`] using [`MinecraftCredentials`] for authentication.
|
||||
/// returns an [`Arc`] pointer to [`RwLock`] to [`ProcessorChild`].
|
||||
#[tracing::instrument(skip(creds))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn run_credentials(
|
||||
path: &ClusterPath,
|
||||
creds: &MinecraftCredentials,
|
||||
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
let cluster = get(path)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to run a nonexistent cluster at path {}", path))?;
|
||||
|
||||
let pre = &cluster
|
||||
.init_hooks
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.init_hooks)
|
||||
.pre
|
||||
.as_ref()
|
||||
.and_then(|it| {
|
||||
if it.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(it.clone())
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(hook) = pre {
|
||||
// TODO: hook parameters
|
||||
let mut cmd = hook.split(' ');
|
||||
if let Some(command) = cmd.next() {
|
||||
let full_path = path.full_path().await?;
|
||||
let result = Command::new(command)
|
||||
.args(cmd)
|
||||
.current_dir(&full_path)
|
||||
.spawn()
|
||||
.map_err(|e| IOError::with_path(e, &full_path))?
|
||||
.wait()
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
|
||||
if !result.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"non-zero exit code for pre-launch hook: {}",
|
||||
result.code().unwrap_or(-1)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let java_args = cluster
|
||||
.java
|
||||
.as_ref()
|
||||
.and_then(|it| it.custom_arguments.as_ref())
|
||||
.unwrap_or(&settings.custom_java_args);
|
||||
|
||||
let wrapper = cluster
|
||||
.init_hooks
|
||||
.as_ref()
|
||||
.map_or(&settings.init_hooks.wrapper, |it| &it.wrapper)
|
||||
.as_ref()
|
||||
.and_then(|it| {
|
||||
if it.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(it.clone())
|
||||
}
|
||||
});
|
||||
|
||||
let memory = cluster.memory.unwrap_or(settings.memory);
|
||||
let resolution = cluster.resolution.unwrap_or(settings.resolution);
|
||||
let env_args = cluster
|
||||
.java
|
||||
.as_ref()
|
||||
.and_then(|it| it.custom_env_arguments.as_ref())
|
||||
.unwrap_or(&settings.custom_env_args);
|
||||
let post = cluster
|
||||
.init_hooks
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.init_hooks)
|
||||
.post
|
||||
.clone();
|
||||
let mut mc_options: Vec<(String, String)> = vec![];
|
||||
if let Some(fullscreen) = cluster.force_fullscreen {
|
||||
mc_options.push(("fullscreen".to_string(), fullscreen.to_string()));
|
||||
} else if settings.force_fullscreen {
|
||||
mc_options.push(("fullscreen".to_string(), "true".to_string()));
|
||||
}
|
||||
|
||||
let process = crate::game::launch_minecraft(
|
||||
&cluster,
|
||||
java_args,
|
||||
env_args,
|
||||
&mc_options,
|
||||
post,
|
||||
creds,
|
||||
&resolution,
|
||||
&memory,
|
||||
&wrapper,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
/// remove a specified cluster from it's [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn remove(path: &ClusterPath) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut clusters = state.clusters.write().await;
|
||||
|
||||
if let Some(cluster) = clusters.remove(path).await? {
|
||||
send_cluster(
|
||||
cluster.uuid,
|
||||
path,
|
||||
&cluster.meta.name,
|
||||
ClusterPayloadType::Deleted,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get a cluster by it's [`uuid::Uuid`]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_by_uuid(uuid: uuid::Uuid) -> crate::Result<Option<Cluster>> {
|
||||
let state = State::get().await?;
|
||||
let clusters = state.clusters.read().await;
|
||||
let cluster = clusters.0.values().find(|c| c.uuid == uuid).cloned();
|
||||
|
||||
Ok(cluster)
|
||||
}
|
||||
|
||||
/// get a cluster's full path by it's [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_full_path(path: &ClusterPath) -> crate::Result<PathBuf> {
|
||||
let _ = get(path).await?.ok_or_else(|| {
|
||||
anyhow::anyhow!("failed to get the full path of cluster at path {}", path)
|
||||
})?;
|
||||
let full_path = io::canonicalize(path.full_path().await?)?;
|
||||
|
||||
Ok(full_path)
|
||||
}
|
||||
|
||||
/// get a specific mod's full path in the filesystem by it's [`ClusterPath`] and [`PackagePath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_package_full_path(
|
||||
cluster_path: &ClusterPath,
|
||||
package_path: &PackagePath,
|
||||
) -> crate::Result<PathBuf> {
|
||||
if get(cluster_path).await?.is_some() {
|
||||
let full_path = io::canonicalize(package_path.full_path(cluster_path).await?)?;
|
||||
|
||||
return Ok(full_path);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"failed to get the full path of a cluster at path {}",
|
||||
package_path
|
||||
.full_path(cluster_path)
|
||||
.await?
|
||||
.display()
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
/// edit a cluster with an async closure and it's [`ClusterPath`]
|
||||
pub async fn edit<FutFn>(
|
||||
path: &ClusterPath,
|
||||
action: impl Fn(&mut Cluster) -> FutFn,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
FutFn: Future<Output = crate::Result<()>>,
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut clusters = state.clusters.write().await;
|
||||
|
||||
match clusters.0.get_mut(path) {
|
||||
Some(ref mut cluster) => {
|
||||
action(cluster).await?;
|
||||
|
||||
send_cluster(
|
||||
cluster.uuid,
|
||||
path,
|
||||
&cluster.meta.name,
|
||||
ClusterPayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow::anyhow!("unmanaged cluster edited at {}", path.to_string()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// update a [`Cluster`]'s icon
|
||||
pub async fn edit_icon(path: &ClusterPath, icon_path: Option<&Path>) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let result = if let Some(icon) = icon_path {
|
||||
let bytes = io::read(icon).await?;
|
||||
let mut clusters = state.clusters.write().await;
|
||||
match clusters.0.get_mut(path) {
|
||||
Some(ref mut cluster) => {
|
||||
cluster
|
||||
.set_icon_bytes(
|
||||
&state.directories.caches_dir().await,
|
||||
&state.io_semaphore,
|
||||
bytes::Bytes::from(bytes),
|
||||
&icon.to_string_lossy(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_cluster(
|
||||
cluster.uuid,
|
||||
path,
|
||||
&cluster.meta.name,
|
||||
ClusterPayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow::anyhow!(
|
||||
"failed to update unmanaged cluster at {}",
|
||||
path.to_string()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
} else {
|
||||
edit(path, |cluster| {
|
||||
cluster.meta.icon = None;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
State::sync().await?;
|
||||
result
|
||||
}
|
||||
|
||||
/// gets the optimal java version for a given [`Cluster`].
|
||||
pub async fn get_optimal_java_version(path: &ClusterPath) -> crate::Result<Option<JavaVersion>> {
|
||||
let state = State::get().await?;
|
||||
if let Some(cluster) = get(path).await? {
|
||||
let metadata = state.metadata.read().await;
|
||||
let minecraft_metadata = metadata
|
||||
.minecraft
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft metadata"))?;
|
||||
|
||||
let version = minecraft_metadata
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == cluster.meta.mc_version)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"invalid or unknown Minecraft version {}",
|
||||
cluster.meta.mc_version
|
||||
)
|
||||
})?;
|
||||
|
||||
let version_info = crate::game::metadata::download_version_info(
|
||||
&state,
|
||||
version,
|
||||
cluster.meta.loader_version.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let version = crate::game::java_version_from_cluster(&cluster, &version_info).await?;
|
||||
|
||||
Ok(version)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"failed to get the java version of unmanaged cluster at {}",
|
||||
path.to_string()
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to update a [`Cluster`]'s playtime.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn update_playtime(path: &ClusterPath) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let cluster = get(path)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to update playtime at path {}", path))?;
|
||||
let recent_playtime = cluster.meta.recently_played;
|
||||
|
||||
/*
|
||||
* todo
|
||||
*/
|
||||
|
||||
let mut clusters = state.clusters.write().await;
|
||||
if let Some(cluster) = clusters.0.get_mut(path) {
|
||||
cluster.meta.overall_played += recent_playtime;
|
||||
cluster.meta.recently_played = 0;
|
||||
}
|
||||
drop(clusters);
|
||||
|
||||
State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This forces the cluster to be verified (and possibly reinstalled).
|
||||
pub async fn repair_cluster(path: &ClusterPath) -> crate::Result<()> {
|
||||
let cluster = get(path)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to repair cluster at path {}", path))?;
|
||||
|
||||
crate::game::install_minecraft(&cluster, None, true).await
|
||||
}
|
||||
|
||||
/// Sanitize a user-inputted [`Cluster`] name.
|
||||
#[must_use]
|
||||
pub fn sanitize_cluster_name(input: &str) -> String {
|
||||
input.replace(['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>'], "_")
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
//! Utilities for updating Clusters
|
||||
|
||||
// TODO: requires `store/package.rs` to be implemented.
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::api::prelude::InternetPayload;
|
||||
use crate::proxy::send::{send_internet, send_message};
|
||||
use onelauncher_utils::io;
|
||||
|
||||
pub async fn parse_uri(cmd: &str) -> crate::Result<()> {
|
||||
let cmd = handle_cmd(cmd).await?;
|
||||
send_internet(cmd).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_cmd(cmd: &str) -> crate::Result<InternetPayload> {
|
||||
tracing::debug!("parsing command {}", &cmd);
|
||||
|
||||
if let Some(sub) = cmd.strip_prefix("onelauncher://") {
|
||||
Ok(handle_uri(sub).await?)
|
||||
} else {
|
||||
let path = io::canonicalize(PathBuf::from(cmd))?;
|
||||
if let Some(extension) = path.extension() {
|
||||
if extension == "zip" {
|
||||
return Ok(InternetPayload::InstallPath { path });
|
||||
}
|
||||
}
|
||||
send_message(&format!("invalid command filetype {}", path.display())).await?;
|
||||
Err(anyhow::anyhow!("invalid command filename {}", path.display()).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_uri(sub: &str) -> crate::Result<InternetPayload> {
|
||||
Ok(match sub.split_once('/') {
|
||||
// ://pkg/{id} - installs package id
|
||||
Some(("pkg", id)) => InternetPayload::InstallPackage { id: id.to_string() },
|
||||
// ://pack/{id} - installs pack id
|
||||
Some(("pack", id)) => InternetPayload::InstallPack { id: id.to_string() },
|
||||
// unknown uri command: error
|
||||
_ => {
|
||||
send_message(&format!("invalid command {sub}")).await?;
|
||||
return Err(anyhow::anyhow!("invalid command {sub}").into());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
use crate::store::{IngressProcessType, IngressProcessor};
|
||||
|
||||
pub async fn check_ingress_feeds() -> crate::Result<bool> {
|
||||
IngressProcessor::finished(IngressProcessType::IngressFeed).await
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::proxy::send::{init_ingress, send_ingress};
|
||||
use crate::utils::http::{fetch_advanced, fetch_json};
|
||||
use crate::utils::java::{self, get_java_version, JavaVersion};
|
||||
use crate::State;
|
||||
use onelauncher_utils::io::{self, IOError};
|
||||
|
||||
pub async fn filter_java_version(java_version: Option<u32>) -> crate::Result<Vec<JavaVersion>> {
|
||||
let java = java::locate_java().await?;
|
||||
Ok(if let Some(java_version) = java_version {
|
||||
java.into_iter()
|
||||
.filter(|j| {
|
||||
let jre_version = get_java_version(&j.version);
|
||||
jre_version.map_or(false, |jre_version| jre_version.1 == java_version)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
java
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct JavaZuluPackage {
|
||||
pub download_url: String,
|
||||
pub name: PathBuf,
|
||||
pub java_version: Vec<u32>,
|
||||
}
|
||||
|
||||
pub async fn get_zulu_packages() -> crate::Result<Vec<JavaZuluPackage>> {
|
||||
let state = State::get().await?;
|
||||
fetch_json::<Vec<JavaZuluPackage>>(
|
||||
Method::GET,
|
||||
format!(
|
||||
"https://api.azul.com/metadata/v1/zulu/packages/?os={}&arch={}&archive_type=zip&java_package_type=jre&javafx_bundled=false&latest=true&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100",
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
).as_str(),
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// TODO: support more than just zulu ?
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn install_java_from_major(java_version: u32) -> crate::Result<PathBuf> {
|
||||
let packages = get_zulu_packages().await?;
|
||||
let package = packages
|
||||
.into_iter()
|
||||
.find(|p| p.java_version.contains(&java_version))
|
||||
.ok_or_else(|| anyhow::anyhow!(
|
||||
"Could not find a java package for version {}",
|
||||
java_version
|
||||
))?;
|
||||
|
||||
install_java_from_package(package).await
|
||||
}
|
||||
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn install_java_from_package(download: JavaZuluPackage) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
let java_version = *download.java_version.first().unwrap_or(&0);
|
||||
|
||||
let ingress = init_ingress(
|
||||
crate::IngressType::DownloadJava {
|
||||
version: java_version,
|
||||
},
|
||||
100.0,
|
||||
"downloading java version",
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_ingress(&ingress, 0.0, Some("downloading java version")).await?;
|
||||
|
||||
let file = fetch_advanced(
|
||||
Method::GET,
|
||||
&download.download_url,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some((&ingress, 80.0)),
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = state.directories.java_dir().await;
|
||||
let mut archive =
|
||||
zip::ZipArchive::new(std::io::Cursor::new(file)).map_err(IOError::from_zip)?;
|
||||
|
||||
if let Some(file) = archive.file_names().next() {
|
||||
if let Some(dir) = file.split('/').next() {
|
||||
let path = path.join(dir);
|
||||
if path.exists() {
|
||||
io::remove_dir_all(path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_ingress(&ingress, 0.0, Some("extracing java binary")).await?;
|
||||
archive.extract(&path).map_err(IOError::from_zip)?;
|
||||
send_ingress(&ingress, 10.0, Some("extracted java binary")).await?;
|
||||
let mut base_path = path.join(
|
||||
download
|
||||
.name
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
base_path = base_path
|
||||
.join(format!("zulu-{java_version}.jre"))
|
||||
.join("Contents")
|
||||
.join("Home")
|
||||
.join("bin")
|
||||
.join("java");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
base_path = base_path.join("bin").join(crate::constants::JAVA_BIN);
|
||||
}
|
||||
|
||||
send_ingress(&ingress, 100.0, Some("installed java binary")).await?;
|
||||
|
||||
Ok(base_path)
|
||||
}
|
||||
|
||||
pub async fn check_java(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
|
||||
Ok(java::check_java_instance(&path).await)
|
||||
}
|
||||
|
||||
pub async fn test_java(path: PathBuf, major: u32) -> crate::Result<bool> {
|
||||
let Some(jvm) = java::check_java_instance(&path).await else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let (maj, _) = get_java_version(&jvm.version)?;
|
||||
Ok(maj == major)
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
//! Metadata management interface
|
||||
|
||||
use crate::State;
|
||||
pub use interpulse::api::minecraft::VersionManifest;
|
||||
pub use interpulse::api::modded::Manifest;
|
||||
|
||||
// TODO: better error handling
|
||||
// TODO: fix this lmao
|
||||
|
||||
/// Get a [`VersionManifest`] for all available Minecraft versions.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_minecraft_versions() -> crate::Result<VersionManifest> {
|
||||
let state = State::get().await?;
|
||||
let meta = state
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.minecraft
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft metadata"))?;
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// Get a [`Manifest`] for all available Fabric versions.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_fabric_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let meta = state
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.fabric
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get fabric metadata"))?;
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// Get a [`Manifest`] for all available Quilt versions.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_quilt_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let meta = state
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.quilt
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get quilt metadata"))?;
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// Get a [`Manifest`] for all available Forge versions.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_forge_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let meta = state
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.forge
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get forge metadata"))?;
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// Get a [`Manifest`] for all available NeoForge versions.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_neoforge_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let meta = state
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.neoforge
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get neoforge metadata"))?;
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
// /// Get a [`Manifest`] for all available Legacy Fabric versions.
|
||||
// #[tracing::instrument]
|
||||
// pub async fn get_legacy_fabric_versions() -> crate::Result<Manifest> {
|
||||
// let state = State::get().await?;
|
||||
// let meta = state
|
||||
// .metadata
|
||||
// .read()
|
||||
// .await
|
||||
// .legacy_fabric
|
||||
// .clone()
|
||||
// .ok_or_else(|| anyhow!("missing legacyfabric metadata"))?;
|
||||
|
||||
// Ok(meta)
|
||||
// }
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
//! Authentication flow manager
|
||||
|
||||
use crate::store::{MinecraftCredentials, MinecraftLogin, State};
|
||||
|
||||
/// Begin a Microsoft authentication flow.
|
||||
#[tracing::instrument]
|
||||
pub async fn begin() -> crate::Result<MinecraftLogin> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.begin().await
|
||||
}
|
||||
|
||||
/// Complete a Microsoft authentication flow to recieve [`MinecraftCredentials`].
|
||||
#[tracing::instrument]
|
||||
pub async fn finish(code: &str, flow: MinecraftLogin) -> crate::Result<MinecraftCredentials> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.finish(code, flow).await
|
||||
}
|
||||
|
||||
/// Get the current default user if it exists by [`uuid::Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
|
||||
Ok(users.default_user)
|
||||
}
|
||||
|
||||
/// Set the current default user by [`uuid::Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn set_default_user(user: Option<uuid::Uuid>) -> crate::Result<()> {
|
||||
let user = match user {
|
||||
Some(user) => Some(get_user(user).await?.id),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.default_user = user;
|
||||
users.save().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a user account by its [`uuid::Uuid`] from the global store.
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.remove(user).await?;
|
||||
|
||||
if users.default_user == Some(user) {
|
||||
set_default_user(Some(user)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a list of user [`MinecraftCredentials`].
|
||||
#[tracing::instrument]
|
||||
pub async fn users() -> crate::Result<Vec<MinecraftCredentials>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
|
||||
Ok(users.users.values().cloned().collect())
|
||||
}
|
||||
|
||||
/// Get a specifc user's [`MinecraftCredentials`] by their [`uuid::Uuid`].
|
||||
/// Use [`crate::store::MinecraftState#refresh`] instead.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user(user: uuid::Uuid) -> crate::Result<MinecraftCredentials> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
|
||||
let user = users
|
||||
.users
|
||||
.get(&user)
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to get nonexistent user with uuid {user}"))?
|
||||
.clone();
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
//! **`OneLauncher` API**
|
||||
//!
|
||||
//! API for interacting with our Rust core.
|
||||
|
||||
pub mod cluster;
|
||||
pub mod handler;
|
||||
pub mod ingress;
|
||||
pub mod java;
|
||||
pub mod metadata;
|
||||
pub mod minecraft;
|
||||
pub mod package;
|
||||
pub mod processor;
|
||||
pub mod proxy;
|
||||
pub mod settings;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::store::{
|
||||
ClusterMeta, Credentials, Directories, InitHooks, JavaOptions, Loader, ManagedDependency,
|
||||
ManagedPackage, ManagedUser, ManagedVersion, Memory, MinecraftCredentials, PackageData,
|
||||
PackageType, Resolution, Settings,
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::cluster::{self, create, Cluster};
|
||||
pub use crate::data::*;
|
||||
pub use crate::proxy::InternetPayload;
|
||||
pub use crate::store::{ClusterPath, JavaVersions, PackageDependency, PackagePath};
|
||||
pub use crate::utils::java::JavaVersion;
|
||||
pub use crate::{java, metadata, minecraft, package, processor, settings, State};
|
||||
}
|
||||
|
|
@ -1,711 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use url::Url;
|
||||
|
||||
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
|
||||
use crate::store::{ManagedVersionFile, ManagedVersionReleaseType, ProviderSearchResults, SearchResult};
|
||||
use crate::utils::{http, pagination::Pagination};
|
||||
use crate::{Result, State};
|
||||
|
||||
/// Mapping of Curseforge "classes" to their respective category ids
|
||||
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u32)]
|
||||
pub enum CFPackageType {
|
||||
#[default]
|
||||
Mods = 6,
|
||||
ModPack = 4471,
|
||||
DataPack = 6945,
|
||||
ResourcePack = 12,
|
||||
ShaderPack = 6552,
|
||||
}
|
||||
|
||||
impl From<CFPackageType> for PackageType {
|
||||
fn from(package_type: CFPackageType) -> Self {
|
||||
match package_type {
|
||||
CFPackageType::Mods => PackageType::Mod,
|
||||
CFPackageType::ModPack => PackageType::ModPack,
|
||||
CFPackageType::DataPack => PackageType::DataPack,
|
||||
CFPackageType::ResourcePack => PackageType::ResourcePack,
|
||||
CFPackageType::ShaderPack => PackageType::ShaderPack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageType> for CFPackageType {
|
||||
fn from(package_type: PackageType) -> Self {
|
||||
match package_type {
|
||||
PackageType::Mod => CFPackageType::Mods,
|
||||
PackageType::ModPack => CFPackageType::ModPack,
|
||||
PackageType::DataPack => CFPackageType::DataPack,
|
||||
PackageType::ResourcePack => CFPackageType::ResourcePack,
|
||||
PackageType::ShaderPack => CFPackageType::ShaderPack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mapping of Curseforge supported loaders to their respective id
|
||||
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum CurseforgeLoader {
|
||||
#[default]
|
||||
Any = 0,
|
||||
Forge = 1,
|
||||
Fabric = 4,
|
||||
Quilt = 5,
|
||||
NeoForge = 6,
|
||||
}
|
||||
|
||||
impl From<String> for CurseforgeLoader {
|
||||
fn from(loader: String) -> Self {
|
||||
match loader.to_lowercase().as_str() {
|
||||
"forge" => CurseforgeLoader::Forge,
|
||||
"fabric" => CurseforgeLoader::Fabric,
|
||||
"quilt" => CurseforgeLoader::Quilt,
|
||||
"neoforge" => CurseforgeLoader::NeoForge,
|
||||
_ => CurseforgeLoader::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Loader> for CurseforgeLoader {
|
||||
fn from(loader: Loader) -> Self {
|
||||
match loader {
|
||||
Loader::Forge => CurseforgeLoader::Forge,
|
||||
Loader::Fabric => CurseforgeLoader::Fabric,
|
||||
Loader::Quilt => CurseforgeLoader::Quilt,
|
||||
Loader::NeoForge => CurseforgeLoader::NeoForge,
|
||||
_ => CurseforgeLoader::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Loader> for CurseforgeLoader {
|
||||
fn into(self) -> Loader {
|
||||
match self {
|
||||
CurseforgeLoader::Forge => Loader::Forge,
|
||||
CurseforgeLoader::Fabric => Loader::Fabric,
|
||||
CurseforgeLoader::Quilt => Loader::Quilt,
|
||||
CurseforgeLoader::NeoForge => Loader::NeoForge,
|
||||
_ => Loader::Vanilla,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CurseforgePackage {
|
||||
pub id: u32,
|
||||
pub game_id: u32,
|
||||
#[serde(alias = "classId")]
|
||||
pub package_type: CFPackageType,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub links: Links,
|
||||
pub summary: String,
|
||||
// pub status: u32,
|
||||
pub download_count: u64,
|
||||
#[serde(default)]
|
||||
pub is_featured: bool,
|
||||
// pub primary_category_id: u32,
|
||||
pub categories: Vec<Category>,
|
||||
pub authors: Vec<Author>,
|
||||
pub logo: Option<Logo>,
|
||||
// pub screenshots: Vec<Screenshot>,
|
||||
// pub main_file_id: u32,
|
||||
// pub latest_files: Vec<LatestFile>,
|
||||
// pub latest_files_indexes: Vec<LatestFilesIndex>,
|
||||
// pub latest_early_access_files_indexes: Vec<LatestEarlyAccessFilesIndex>,
|
||||
pub date_created: DateTime<Utc>,
|
||||
pub date_modified: DateTime<Utc>,
|
||||
pub date_released: DateTime<Utc>,
|
||||
// #[serde(default)]
|
||||
// pub allow_mod_distribution: bool,
|
||||
// pub game_popularity_rank: u32,
|
||||
// pub is_available: bool,
|
||||
pub thumbs_up_count: u32,
|
||||
pub rating: Option<f32>,
|
||||
}
|
||||
|
||||
impl From<CurseforgePackage> for ManagedPackage {
|
||||
fn from(package: CurseforgePackage) -> Self {
|
||||
ManagedPackage {
|
||||
id: package.id.to_string(),
|
||||
title: package.name,
|
||||
provider: super::Providers::Curseforge,
|
||||
package_type: package.package_type.into(),
|
||||
description: package.summary,
|
||||
body: crate::store::PackageBody::Url(format!("/v1/mods/{}/description", package.id)),
|
||||
main: package.slug,
|
||||
versions: vec![],
|
||||
game_versions: vec![],
|
||||
loaders: vec![],
|
||||
icon_url: package.logo.and_then(|l| Some(l.url)),
|
||||
created: Some(package.date_created),
|
||||
updated: Some(package.date_modified),
|
||||
client: crate::store::PackageSide::Unknown,
|
||||
server: crate::store::PackageSide::Unknown,
|
||||
downloads: package.download_count,
|
||||
followers: package.rating.unwrap_or(0.0) as u32,
|
||||
categories: vec![], // TODO
|
||||
optional_categories: None,
|
||||
license: None,
|
||||
author: crate::store::Author::Users(
|
||||
package
|
||||
.authors
|
||||
.into_iter()
|
||||
.map(|a| ManagedUser {
|
||||
id: a.id.to_string(),
|
||||
username: a.name,
|
||||
url: Some(a.url),
|
||||
is_organization_user: false,
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
role: None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
is_archived: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<SearchResult> for CurseforgePackage {
|
||||
fn into(self) -> SearchResult {
|
||||
SearchResult {
|
||||
title: self.name,
|
||||
slug: self.slug,
|
||||
project_id: self.id.to_string(),
|
||||
author: self
|
||||
.authors
|
||||
.first()
|
||||
.and_then(|a| Some(a.name.clone()))
|
||||
.unwrap_or(String::from("Unknown")),
|
||||
description: self.summary,
|
||||
client_side: crate::store::PackageSide::Unknown,
|
||||
server_side: crate::store::PackageSide::Unknown,
|
||||
project_type: self.package_type.into(),
|
||||
downloads: self.download_count,
|
||||
icon_url: self.logo.map_or(String::new(), |l| l.url),
|
||||
categories: vec![], // TODO
|
||||
versions: vec![],
|
||||
follows: self.thumbs_up_count,
|
||||
date_created: Some(self.date_created),
|
||||
date_modified: Some(self.date_modified),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Links {
|
||||
pub website_url: Option<String>,
|
||||
pub wiki_url: Option<String>,
|
||||
pub issues_url: Option<String>,
|
||||
pub source_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Category {
|
||||
pub id: u32,
|
||||
pub game_id: u32,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub url: String,
|
||||
pub icon_url: String,
|
||||
pub date_modified: String,
|
||||
// pub parent_category_id: u32,
|
||||
// pub display_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Author {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Logo {
|
||||
pub id: u32,
|
||||
pub mod_id: u32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub thumbnail_url: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Screenshot {
|
||||
pub id: u32,
|
||||
pub mod_id: u32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub thumbnail_url: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize_repr, Deserialize_repr, Clone, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum ModFileReleaseType {
|
||||
#[default]
|
||||
Release = 1,
|
||||
Beta = 2,
|
||||
Alpha = 3,
|
||||
}
|
||||
|
||||
impl Into<ManagedVersionReleaseType> for ModFileReleaseType {
|
||||
fn into(self) -> ManagedVersionReleaseType {
|
||||
match self {
|
||||
ModFileReleaseType::Release => ManagedVersionReleaseType::Release,
|
||||
ModFileReleaseType::Beta => ManagedVersionReleaseType::Beta,
|
||||
ModFileReleaseType::Alpha => ManagedVersionReleaseType::Alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModFile {
|
||||
pub id: u32,
|
||||
pub game_id: u32,
|
||||
pub mod_id: u32,
|
||||
#[serde(default)]
|
||||
pub is_available: bool,
|
||||
pub display_name: String,
|
||||
pub file_name: String,
|
||||
pub release_type: ModFileReleaseType,
|
||||
// pub file_status: u32,
|
||||
pub hashes: Vec<Hash>,
|
||||
pub file_date: DateTime<Utc>,
|
||||
pub file_length: u64,
|
||||
pub download_count: u32,
|
||||
pub file_size_on_disk: Option<u64>,
|
||||
pub download_url: Option<String>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub sortable_game_versions: Vec<SortableGameVersion>,
|
||||
pub dependencies: Vec<Dependency>,
|
||||
// pub expose_as_alternative: bool,
|
||||
// pub parent_project_file_id: u32,
|
||||
// pub alternate_file_id: u32,
|
||||
// pub is_server_pack: bool,
|
||||
// pub server_pack_file_id: u32,
|
||||
// pub is_early_access_content: bool,
|
||||
// pub early_access_end_date: String,
|
||||
pub file_fingerprint: u32,
|
||||
// pub modules: Vec<Module>,
|
||||
}
|
||||
|
||||
impl Into<ManagedVersion> for ModFile {
|
||||
fn into(self) -> ManagedVersion {
|
||||
let mut hashes = HashMap::new();
|
||||
|
||||
for hash in self.hashes {
|
||||
hashes.insert(hash.algo.name(), hash.value);
|
||||
}
|
||||
|
||||
let mut game_versions = Vec::new();
|
||||
let mut loader = CurseforgeLoader::Any;
|
||||
|
||||
for version in self.sortable_game_versions {
|
||||
if version.game_version.is_empty() {
|
||||
// This is most likely a loader
|
||||
loader = CurseforgeLoader::from(version.game_version_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
game_versions.push(version.game_version);
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
if let Some(download_url) = self.download_url {
|
||||
files.push(ManagedVersionFile {
|
||||
url: download_url,
|
||||
file_name: self.file_name,
|
||||
primary: true,
|
||||
size: self.file_size_on_disk.unwrap_or(self.file_length),
|
||||
file_type: Some(crate::store::PackageFile::RequiredPack),
|
||||
hashes,
|
||||
});
|
||||
}
|
||||
|
||||
ManagedVersion {
|
||||
package_id: self.mod_id.to_string(),
|
||||
id: self.id.to_string(),
|
||||
name: self.display_name.clone(),
|
||||
author: String::new(),
|
||||
loaders: vec![Into::<Loader>::into(loader)],
|
||||
changelog: String::new(),
|
||||
changelog_url: None,
|
||||
deps: vec![],
|
||||
downloads: self.download_count,
|
||||
featured: false,
|
||||
is_available: self.is_available && files.len() > 0,
|
||||
files,
|
||||
game_versions,
|
||||
published: Some(self.file_date),
|
||||
version_display: self.display_name,
|
||||
version_type: self.release_type.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum HashAlgorithm {
|
||||
Sha1 = 1,
|
||||
MD5 = 2,
|
||||
}
|
||||
|
||||
impl HashAlgorithm {
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Self::Sha1 => "sha1",
|
||||
Self::MD5 => "md5",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hash {
|
||||
pub value: String,
|
||||
pub algo: HashAlgorithm,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SortableGameVersion {
|
||||
pub game_version_name: String,
|
||||
pub game_version_padded: String,
|
||||
pub game_version: String,
|
||||
pub game_version_release_date: String,
|
||||
pub game_version_type_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Dependency {
|
||||
pub mod_id: u32,
|
||||
pub relation_type: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Module {
|
||||
pub name: String,
|
||||
pub fingerprint: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModFilesIndex {
|
||||
pub game_version: String,
|
||||
pub file_id: u32,
|
||||
pub filename: String,
|
||||
pub release_type: u32,
|
||||
pub game_version_type_id: u32,
|
||||
#[serde(default)]
|
||||
pub mod_loader: CurseforgeLoader,
|
||||
}
|
||||
|
||||
// https://docs.curseforge.com/rest-api/?shell#tocS_ModsSearchSortField
|
||||
#[allow(dead_code)]
|
||||
enum SearchSort {
|
||||
Featured = 1,
|
||||
Popularity = 2,
|
||||
LastUpdated = 3,
|
||||
Name = 4,
|
||||
Author = 5,
|
||||
TotalDownloads = 6,
|
||||
Rating = 12,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct CFData<T> {
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct CFPaginatedData<T> {
|
||||
pagination: Pagination,
|
||||
data: T,
|
||||
}
|
||||
|
||||
pub async fn fetch_advanced<F: FnOnce(&mut Url)>(
|
||||
method: Method,
|
||||
url: &str,
|
||||
url_builder: Option<F>,
|
||||
headers: Option<HashMap<&str, &str>>,
|
||||
json_body: Option<serde_json::Value>,
|
||||
) -> Result<Bytes> {
|
||||
// TODO: Get the API key from settings, fallback to constant, and error if missing
|
||||
let key = crate::constants::CURSEFORGE_API_KEY;
|
||||
|
||||
let mut headers = headers.unwrap_or(HashMap::<&str, &str>::new());
|
||||
headers.insert("x-api-key", key);
|
||||
|
||||
let url =
|
||||
&mut Url::parse(format!("{}{}", crate::constants::CURSEFORGE_API_URL, url).as_str())?;
|
||||
|
||||
if let Some(url_builder) = url_builder {
|
||||
url_builder(url);
|
||||
}
|
||||
|
||||
http::fetch_advanced(
|
||||
method,
|
||||
url.as_str(),
|
||||
None,
|
||||
json_body,
|
||||
Some(headers),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_url_builder<F: FnOnce(&mut Url)>(
|
||||
url: &str,
|
||||
url_builder: Option<F>,
|
||||
) -> Result<Bytes> {
|
||||
fetch_advanced(Method::GET, url, url_builder, None, None).await
|
||||
}
|
||||
|
||||
async fn fetch(url: &str) -> Result<Bytes> {
|
||||
fetch_url_builder(url, None::<fn(&mut Url)>).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn search(
|
||||
query: Option<String>,
|
||||
limit: Option<u8>,
|
||||
offset: Option<u32>,
|
||||
game_versions: Option<Vec<String>>,
|
||||
_categories: Option<Vec<String>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
package_types: Option<Vec<PackageType>>,
|
||||
) -> Result<ProviderSearchResults> {
|
||||
type CFSearchResults = CFPaginatedData<Vec<CurseforgePackage>>;
|
||||
|
||||
let results: CFSearchResults = serde_json::from_slice(
|
||||
&fetch_url_builder(
|
||||
"/v1/mods/search",
|
||||
Some(Box::new(move |url: &mut Url| {
|
||||
let params = &mut url.query_pairs_mut();
|
||||
|
||||
params.append_pair(
|
||||
"gameId",
|
||||
crate::constants::CURSEFORGE_GAME_ID.to_string().as_str(),
|
||||
);
|
||||
params.append_pair("pageSize", limit.unwrap_or(10).to_string().as_str());
|
||||
params.append_pair("index", offset.unwrap_or(0).to_string().as_str());
|
||||
params.append_pair(
|
||||
"sortField",
|
||||
(SearchSort::Popularity as u8).to_string().as_str(),
|
||||
);
|
||||
params.append_pair("sortOrder", "desc");
|
||||
|
||||
if let Some(query) = query {
|
||||
params.append_pair("searchFilter", query.as_str());
|
||||
}
|
||||
|
||||
if let Some(game_versions) = game_versions {
|
||||
// Need to create a list e.g. ["1.16.5", "1.17.1"]
|
||||
params.append_pair(
|
||||
"gameVersions",
|
||||
format!(
|
||||
"[{}]",
|
||||
game_versions
|
||||
.into_iter()
|
||||
.map(|v| format!("\"{}\"", v))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(loaders) = loaders {
|
||||
params.append_pair(
|
||||
"modLoaderTypes",
|
||||
format!(
|
||||
"[{}]",
|
||||
loaders
|
||||
.into_iter()
|
||||
.map(|l| (CurseforgeLoader::from(l) as u32).to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(package_types) = package_types {
|
||||
if package_types.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if package_types.len() > 1 {
|
||||
tracing::warn!(
|
||||
"curseforge provider does not support multiple package types"
|
||||
);
|
||||
}
|
||||
|
||||
let package_type = package_types.first().unwrap();
|
||||
|
||||
params.append_pair(
|
||||
"classId",
|
||||
(CFPackageType::from(*package_type) as u32)
|
||||
.to_string()
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
Ok(ProviderSearchResults {
|
||||
total: results.pagination.total_count,
|
||||
results: results
|
||||
.data
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<SearchResult>>(),
|
||||
provider: crate::package::content::Providers::Curseforge,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get(id: u32) -> Result<CurseforgePackage> {
|
||||
Ok(
|
||||
serde_json::from_slice::<CFData<_>>(&fetch(format!("/v1/mods/{}", id).as_str()).await?)?
|
||||
.data,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_multiple(ids: &[u32]) -> Result<Vec<CurseforgePackage>> {
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert("modIds".to_string(), ids.to_vec().into());
|
||||
|
||||
let json_body = serde_json::Value::Object(map);
|
||||
|
||||
Ok(
|
||||
serde_json::from_slice::<CFData<_>>(
|
||||
&fetch_advanced(
|
||||
Method::POST,
|
||||
"/v1/mods",
|
||||
None::<fn(&mut Url)>,
|
||||
None,
|
||||
Some(json_body),
|
||||
).await?)?
|
||||
.data,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_package_body(url: String) -> Result<String> {
|
||||
Ok(serde_json::from_slice::<CFData<_>>(&fetch(&url).await?)?.data)
|
||||
}
|
||||
|
||||
pub async fn get_all_versions(
|
||||
project_id: &str,
|
||||
game_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
page: Option<u32>,
|
||||
page_size: Option<u16>,
|
||||
) -> Result<(Vec<ModFile>, Pagination)> {
|
||||
let data: CFPaginatedData<Vec<ModFile>> = serde_json::from_slice::<CFPaginatedData<_>>(
|
||||
&fetch_url_builder(
|
||||
format!("/v1/mods/{project_id}/files").as_str(),
|
||||
Some(Box::new(move |url: &mut Url| {
|
||||
if let Some(game_versions) = game_versions {
|
||||
if let Some(version) = game_versions.first() {
|
||||
url.query_pairs_mut().append_pair("gameVersion", version);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(loader) = loaders {
|
||||
if let Some(loader) = loader.first() {
|
||||
url.query_pairs_mut().append_pair(
|
||||
"modLoaderType",
|
||||
(CurseforgeLoader::from(*loader) as u8).to_string().as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let page = page.unwrap_or(0);
|
||||
url.query_pairs_mut().append_pair("index", page.to_string().as_str());
|
||||
|
||||
let page_size = page_size.unwrap_or(10);
|
||||
url.query_pairs_mut().append_pair("pageSize", page_size.to_string().as_str());
|
||||
})),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
Ok((data.data, data.pagination))
|
||||
}
|
||||
|
||||
pub async fn get_versions(
|
||||
versions: Vec<String>,
|
||||
) -> Result<Vec<ModFile>> {
|
||||
let json_body = json!({
|
||||
"fileIds": versions.iter().map(|v| v.parse::<u32>().unwrap()).collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
Ok(serde_json::from_slice::<CFData<_>>(
|
||||
&fetch_advanced(
|
||||
Method::POST,
|
||||
format!("/v1/mods/files").as_str(),
|
||||
None::<fn(&mut Url)>,
|
||||
None,
|
||||
Some(json_body)
|
||||
)
|
||||
.await?,
|
||||
)?
|
||||
.data)
|
||||
}
|
||||
|
||||
pub async fn get_versions_by_hashes(
|
||||
hashes: Vec<String>,
|
||||
) -> Result<HashMap<String, ModFile>> {
|
||||
let body = json!({
|
||||
"fingerprints": hashes.into_iter().map(|hash| hash.parse::<u32>().unwrap_or_default()).collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Response {
|
||||
exact_matches: Vec<FingerprintMatch>,
|
||||
exact_fingerprints: Vec<u32>
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct FingerprintMatch {
|
||||
id: u32,
|
||||
file: ModFile
|
||||
}
|
||||
|
||||
let files = serde_json::from_slice::<CFData<Response>>(
|
||||
&fetch_advanced(
|
||||
Method::POST,
|
||||
format!("/v1/fingerprints/{}", crate::constants::CURSEFORGE_GAME_ID).as_str(),
|
||||
None::<fn(&mut Url)>,
|
||||
None,
|
||||
Some(body),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
Ok(files.data.exact_matches.into_iter().map(|m| (m.file.file_fingerprint.to_string(), m.file)).collect())
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,337 +0,0 @@
|
|||
//! **`OneLauncher` Content Package**
|
||||
//!
|
||||
//! Utilities for searching and downloading content packages to `OneLauncher`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use modrinth::{Facet, FacetOperation};
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
|
||||
use crate::package::content::modrinth::FacetBuilder;
|
||||
use crate::store::{Author, PackageBody, ProviderSearchResults};
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::http::fetch_json;
|
||||
use crate::utils::pagination::Pagination;
|
||||
use crate::{Result, State};
|
||||
|
||||
mod curseforge;
|
||||
mod modrinth;
|
||||
mod skyclient;
|
||||
|
||||
/// Providers for content packages
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
|
||||
pub enum Providers {
|
||||
Modrinth,
|
||||
Curseforge,
|
||||
SkyClient,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Providers {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl Providers {
|
||||
/// Get the name of the provider
|
||||
#[must_use]
|
||||
pub const fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Modrinth => "Modrinth",
|
||||
Self::Curseforge => "Curseforge",
|
||||
Self::SkyClient => "SkyClient",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the URL of the provider
|
||||
#[must_use]
|
||||
pub const fn url(&self) -> &str {
|
||||
match self {
|
||||
Self::Modrinth => "https://modrinth.com",
|
||||
Self::Curseforge => "https://curseforge.com",
|
||||
Self::SkyClient => "https://skyclient.co",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_providers() -> &'static [Providers] {
|
||||
&[Self::Modrinth, Self::Curseforge, Self::SkyClient]
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn search(
|
||||
&self,
|
||||
query: Option<String>,
|
||||
limit: Option<u8>,
|
||||
offset: Option<u32>,
|
||||
game_versions: Option<Vec<String>>,
|
||||
categories: Option<Vec<String>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
package_types: Option<Vec<PackageType>>,
|
||||
open_source: Option<bool>,
|
||||
) -> Result<ProviderSearchResults> {
|
||||
match self {
|
||||
Self::Modrinth => {
|
||||
modrinth::search(
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
Some(|mut builder| {
|
||||
build_facets(
|
||||
&mut builder,
|
||||
categories,
|
||||
game_versions,
|
||||
loaders,
|
||||
package_types,
|
||||
open_source,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Self::Curseforge => {
|
||||
curseforge::search(
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
game_versions,
|
||||
categories,
|
||||
loaders,
|
||||
package_types,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Self::SkyClient => {
|
||||
skyclient::search(
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
game_versions,
|
||||
loaders
|
||||
).await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, slug_or_id: &str) -> Result<ManagedPackage> {
|
||||
Ok(match self {
|
||||
Self::Modrinth => modrinth::get(slug_or_id).await?.into(),
|
||||
Self::Curseforge => curseforge::get(slug_or_id.parse::<u32>().map_err(|err| anyhow::anyhow!(err))?).await?.into(),
|
||||
Self::SkyClient => skyclient::get(slug_or_id).await?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_multiple(&self, slug_or_ids: &[String]) -> Result<Vec<ManagedPackage>> {
|
||||
if slug_or_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
Ok(match self {
|
||||
Self::Modrinth => modrinth::get_multiple(slug_or_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
Self::Curseforge => {
|
||||
let parsed_ids = slug_or_ids
|
||||
.iter()
|
||||
.filter_map(|id| id.parse::<u32>().ok())
|
||||
.collect::<Vec<u32>>();
|
||||
|
||||
if parsed_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let parsed_ids = parsed_ids.as_slice();
|
||||
|
||||
curseforge::get_multiple(parsed_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
},
|
||||
Self::SkyClient => skyclient::get_multiple(slug_or_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_all_versions(
|
||||
&self,
|
||||
project_id: &str,
|
||||
game_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
page: Option<u32>,
|
||||
page_size: Option<u16>,
|
||||
) -> Result<(Vec<ManagedVersion>, Pagination)> {
|
||||
Ok(match self {
|
||||
Self::Modrinth => {
|
||||
let data = modrinth::get_all_versions(project_id, game_versions, loaders, page, page_size).await?;
|
||||
(data.0.into_iter().map(Into::into).collect(), data.1)
|
||||
}
|
||||
|
||||
Self::Curseforge => {
|
||||
let data = curseforge::get_all_versions(project_id, game_versions, loaders, page, page_size).await?;
|
||||
(data.0.into_iter().map(Into::into).collect(), data.1)
|
||||
}
|
||||
|
||||
Self::SkyClient => {
|
||||
let data = skyclient::get_all_versions(project_id, game_versions, loaders, page, page_size).await?;
|
||||
(data.0, data.1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_versions(&self, versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
|
||||
Ok(match self {
|
||||
Self::Modrinth => modrinth::get_versions(versions)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
Self::Curseforge => curseforge::get_versions(versions)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
Self::SkyClient => skyclient::get_versions(versions).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_authors(&self, author: &Author) -> Result<Vec<ManagedUser>> {
|
||||
Ok(match author {
|
||||
Author::Team { .. } => match self {
|
||||
Self::Modrinth => modrinth::get_authors(author).await?,
|
||||
_ => return Err(anyhow::anyhow!("{} does not support teams", self).into()),
|
||||
},
|
||||
Author::Users(users) => users.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_package_body(&self, body: &PackageBody) -> Result<String> {
|
||||
Ok(match body {
|
||||
PackageBody::Url(url) => match self {
|
||||
Self::Curseforge => curseforge::get_package_body(url.to_owned()).await?,
|
||||
_ => return Err(anyhow::anyhow!("{} does not support direct URLs", self).into()),
|
||||
},
|
||||
PackageBody::Markdown(markdown) => markdown.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_versions_by_hashes(&self, hashes: Vec<String>) -> Result<HashMap<String, ManagedVersion>> {
|
||||
Ok(match self {
|
||||
Self::Modrinth => modrinth::get_versions_by_hashes(hashes)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(hash, version)| (hash, version.into()))
|
||||
.collect(),
|
||||
Self::Curseforge => curseforge::get_versions_by_hashes(hashes)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(hash, version)| (hash, version.into()))
|
||||
.collect(),
|
||||
Self::SkyClient => skyclient::get_versions_by_hashes(hashes).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hash_file(&self, path: &PathBuf) -> Result<String> {
|
||||
match self {
|
||||
Providers::Modrinth => crypto::sha1_file(path),
|
||||
Providers::SkyClient => crypto::md5_file(path),
|
||||
Providers::Curseforge => {
|
||||
let hash = crypto::murmur2_file(path)?;
|
||||
Ok(hash.to_string())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FeaturedPackage {
|
||||
pub package_type: PackageType,
|
||||
pub provider: Providers,
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub thumbnail: String,
|
||||
pub oneconfig: bool,
|
||||
}
|
||||
|
||||
pub async fn get_featured_packages() -> Result<Vec<FeaturedPackage>> {
|
||||
let state = State::get().await?;
|
||||
fetch_json(
|
||||
Method::GET,
|
||||
crate::constants::FEATURED_PACKAGES_URL,
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore
|
||||
).await
|
||||
}
|
||||
|
||||
fn build_facets(
|
||||
builder: &mut FacetBuilder,
|
||||
categories: Option<Vec<String>>,
|
||||
game_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
package_types: Option<Vec<PackageType>>,
|
||||
open_source: Option<bool>,
|
||||
) -> String {
|
||||
if let Some(categories) = categories {
|
||||
for category in categories {
|
||||
builder.and(Facet(
|
||||
"categories".to_string(),
|
||||
FacetOperation::Eq,
|
||||
category,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(game_versions) = game_versions {
|
||||
for version in game_versions {
|
||||
builder.and(Facet("versions".to_string(), FacetOperation::Eq, version));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(package_types) = package_types {
|
||||
if package_types.contains(&PackageType::Mod)
|
||||
|| package_types.contains(&PackageType::ModPack)
|
||||
{
|
||||
if let Some(loaders) = loaders {
|
||||
for loader in loaders {
|
||||
builder.and(Facet(
|
||||
"categories".to_string(),
|
||||
FacetOperation::Eq,
|
||||
loader.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for package_type in package_types {
|
||||
builder.and(Facet(
|
||||
"project_types".to_string(),
|
||||
FacetOperation::Eq,
|
||||
package_type.get_name().to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(open_source) = open_source {
|
||||
builder.and(Facet(
|
||||
"open_source".to_string(),
|
||||
FacetOperation::Eq,
|
||||
open_source.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// builder.and(Facet("client_side".to_string(), FacetOperation::Eq, "")) // TODO: Possibly make this client_side = required
|
||||
|
||||
builder.build()
|
||||
}
|
||||
|
|
@ -1,535 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
|
||||
use crate::store::{
|
||||
Author, License, ManagedVersionFile, ManagedVersionReleaseType, PackageFile, PackageSide, ProviderSearchResults, SearchResult
|
||||
};
|
||||
use crate::utils::http::{fetch, fetch_advanced};
|
||||
use crate::utils::pagination::Pagination;
|
||||
use crate::{Result, State};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::Providers;
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ModrinthPackage {
|
||||
pub slug: String,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub categories: Vec<String>,
|
||||
pub client_side: PackageSide,
|
||||
pub server_side: PackageSide,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "display_categories")]
|
||||
pub additional_categories: Vec<String>,
|
||||
// pub issues_url: String,
|
||||
// pub source_url: String,
|
||||
// pub wiki_url: String,
|
||||
// pub discord_url: String,
|
||||
// pub donation_urls: Vec<DonationUrl>,
|
||||
pub project_type: PackageType,
|
||||
pub downloads: u32,
|
||||
pub icon_url: Option<String>,
|
||||
#[serde(alias = "project_id")]
|
||||
pub id: String,
|
||||
pub team: String,
|
||||
#[serde(default)]
|
||||
pub organization: Option<String>,
|
||||
#[serde(alias = "date_created")]
|
||||
pub published: DateTime<Utc>,
|
||||
#[serde(alias = "date_modified")]
|
||||
pub updated: DateTime<Utc>,
|
||||
pub followers: u32,
|
||||
#[serde(default)]
|
||||
pub versions: Vec<String>,
|
||||
pub game_versions: Vec<String>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub loaders: Vec<Loader>,
|
||||
#[serde(default)]
|
||||
pub license: Option<License>,
|
||||
// #[serde(default)]
|
||||
// pub gallery: Vec<Gallery>,
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl From<ModrinthPackage> for ManagedPackage {
|
||||
fn from(value: ModrinthPackage) -> Self {
|
||||
Self {
|
||||
provider: Providers::Modrinth,
|
||||
id: value.id,
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
body: crate::store::PackageBody::Markdown(value.body),
|
||||
main: value.slug,
|
||||
versions: value.versions,
|
||||
game_versions: value.game_versions,
|
||||
loaders: value.loaders,
|
||||
icon_url: value.icon_url,
|
||||
created: Some(value.published),
|
||||
updated: Some(value.updated),
|
||||
client: value.client_side,
|
||||
server: value.server_side,
|
||||
downloads: value.downloads as u64,
|
||||
followers: value.followers,
|
||||
categories: value.categories,
|
||||
optional_categories: Some(value.additional_categories),
|
||||
package_type: PackageType::Mod,
|
||||
license: value.license,
|
||||
author: Author::Team {
|
||||
team: value.team,
|
||||
organization: value.organization,
|
||||
},
|
||||
is_archived: value.status == "archived",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DonationUrl {
|
||||
pub id: String,
|
||||
pub platform: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Gallery {
|
||||
pub url: String,
|
||||
pub featured: bool,
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
pub created: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub ordering: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SearchResults {
|
||||
hits: Vec<SearchResult>,
|
||||
total_hits: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ModrinthVersion {
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
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<String>,
|
||||
pub date_published: DateTime<Utc>,
|
||||
pub downloads: u32,
|
||||
pub version_type: String,
|
||||
pub status: String,
|
||||
pub requested_status: Value,
|
||||
pub files: Vec<File>,
|
||||
pub dependencies: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct File {
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub primary: bool,
|
||||
pub size: u64,
|
||||
pub file_type: Option<PackageFile>,
|
||||
pub hashes: HashMap<String, String>
|
||||
}
|
||||
|
||||
impl From<File> for ManagedVersionFile {
|
||||
fn from(value: File) -> Self {
|
||||
Self {
|
||||
url: value.url,
|
||||
file_name: value.filename,
|
||||
primary: value.primary,
|
||||
size: value.size,
|
||||
file_type: value.file_type,
|
||||
hashes: value.hashes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ModrinthVersion> for ManagedVersion {
|
||||
fn from(value: ModrinthVersion) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
package_id: value.project_id,
|
||||
author: value.author_id,
|
||||
name: value.name,
|
||||
|
||||
featured: value.featured,
|
||||
version_display: value.version_number,
|
||||
changelog: value.changelog,
|
||||
changelog_url: value.changelog_url,
|
||||
|
||||
published: Some(value.date_published),
|
||||
downloads: value.downloads,
|
||||
version_type: ManagedVersionReleaseType::from(value.version_type),
|
||||
|
||||
files: value.files.into_iter().map(Into::into).collect(),
|
||||
is_available: true,
|
||||
deps: vec![], // TODO [`ManagedDependency`]?
|
||||
game_versions: value.game_versions,
|
||||
loaders: value
|
||||
.loaders
|
||||
.into_iter()
|
||||
.filter_map(|loader| Loader::try_from(loader).ok())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum FacetOperation {
|
||||
NotEq,
|
||||
LargeEq,
|
||||
Large,
|
||||
SmallEq,
|
||||
Small,
|
||||
Eq,
|
||||
}
|
||||
|
||||
impl Display for FacetOperation {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::NotEq => "!=",
|
||||
Self::LargeEq => ">=",
|
||||
Self::Large => ">",
|
||||
Self::SmallEq => "<=",
|
||||
Self::Small => "<",
|
||||
Self::Eq => "=",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Facet(pub String, pub FacetOperation, pub String);
|
||||
|
||||
impl Display for Facet {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(format!("{}{}{}", self.0, self.1, self.2).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FacetBuilder {
|
||||
pub facets: Vec<Vec<Facet>>,
|
||||
}
|
||||
|
||||
impl FacetBuilder {
|
||||
pub const fn builder() -> Self {
|
||||
Self { facets: vec![] }
|
||||
}
|
||||
|
||||
pub fn and(&mut self, facet: Facet) -> &Self {
|
||||
self.facets.push(vec![facet]);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn or(&mut self, facet: Facet) -> &Self {
|
||||
let mut last_facet = self.facets.pop().unwrap_or_default();
|
||||
last_facet.push(facet);
|
||||
self.facets.push(last_facet);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(&self) -> String {
|
||||
let mut builder: Vec<String> = vec![];
|
||||
|
||||
for facet in &self.facets {
|
||||
let mut stringified = String::new();
|
||||
stringified.push('[');
|
||||
for (i, f) in facet.iter().enumerate() {
|
||||
if i != 0 {
|
||||
stringified.push(',');
|
||||
}
|
||||
stringified.push_str(format!("\"{f}\"").as_str());
|
||||
}
|
||||
stringified.push(']');
|
||||
|
||||
builder.push(stringified);
|
||||
}
|
||||
|
||||
builder.join(",")
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! format_url {
|
||||
($($arg:tt)*) => {{
|
||||
format!("{}{}", crate::constants::MODRINTH_API_URL, format!($($arg)*))
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! format_url_v3 {
|
||||
($($arg:tt)*) => {{
|
||||
format!("{}{}", crate::constants::MODRINTH_V3_API_URL, format!($($arg)*))
|
||||
}};
|
||||
}
|
||||
|
||||
pub async fn get(id: &str) -> Result<ModrinthPackage> {
|
||||
Ok(serde_json::from_slice(
|
||||
&fetch(
|
||||
format_url!("/project/{}", id).as_str(),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub async fn get_multiple(ids: &[String]) -> Result<Vec<ModrinthPackage>> {
|
||||
Ok(serde_json::from_slice(
|
||||
&fetch(
|
||||
format_url!(
|
||||
"/projects?ids=[{}]",
|
||||
ids.iter()
|
||||
.map(|id| format!("\"{id}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
.as_str(),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub async fn search<F>(
|
||||
query: Option<String>,
|
||||
limit: Option<u8>,
|
||||
offset: Option<u32>,
|
||||
facets: Option<F>,
|
||||
) -> Result<ProviderSearchResults>
|
||||
where
|
||||
F: FnOnce(FacetBuilder) -> String,
|
||||
{
|
||||
let facets = facets.map_or(String::new(), |func| func(FacetBuilder::builder()));
|
||||
|
||||
let results: SearchResults = serde_json::from_slice(
|
||||
&fetch(
|
||||
format_url!(
|
||||
"/search?query={}&limit={}&offset={}{}",
|
||||
query.unwrap_or_default(),
|
||||
limit.unwrap_or(10),
|
||||
offset.unwrap_or(0),
|
||||
if facets.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("&facets=[{facets}]")
|
||||
}
|
||||
)
|
||||
.as_str(),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
Ok(ProviderSearchResults {
|
||||
provider: Providers::Modrinth,
|
||||
results: results.hits,
|
||||
total: results.total_hits,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TeamMember {
|
||||
#[serde(default)]
|
||||
pub role: Option<String>,
|
||||
pub user: ManagedUser,
|
||||
}
|
||||
|
||||
/// Get the authors of a project
|
||||
pub async fn get_authors(author: &Author) -> Result<Vec<ManagedUser>> {
|
||||
match author {
|
||||
Author::Users(users) => Ok(users.clone()),
|
||||
Author::Team { team, organization } => {
|
||||
if let Some(organization) = organization {
|
||||
#[derive(Deserialize)]
|
||||
struct Organization {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub icon_url: String,
|
||||
pub description: String,
|
||||
pub members: Vec<TeamMember>,
|
||||
}
|
||||
|
||||
let organization: Organization = serde_json::from_slice(
|
||||
&fetch(
|
||||
format_url_v3!("/organization/{}", organization).as_str(),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
let mut org_user = ManagedUser {
|
||||
id: organization.id,
|
||||
username: organization.name,
|
||||
avatar_url: Some(organization.icon_url),
|
||||
bio: Some(organization.description),
|
||||
url: None,
|
||||
is_organization_user: true,
|
||||
role: None,
|
||||
};
|
||||
|
||||
org_user.url = Some(format!("https://modrinth.com/organization/{}", org_user.id));
|
||||
|
||||
let members = get_team(team.clone()).await;
|
||||
let members = match members {
|
||||
Ok(members) => members,
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to get team members: {}", err);
|
||||
organization
|
||||
.members
|
||||
.into_iter()
|
||||
.map(|member| member.user)
|
||||
.collect::<Vec<ManagedUser>>()
|
||||
}
|
||||
};
|
||||
|
||||
let mut authors = vec![org_user];
|
||||
authors.extend(members);
|
||||
|
||||
Ok(authors)
|
||||
} else {
|
||||
get_team(team.clone()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_team(team_id: String) -> Result<Vec<ManagedUser>> {
|
||||
let authors: Vec<TeamMember> = serde_json::from_slice(
|
||||
&fetch(
|
||||
format_url!("/team/{}/members", team_id).as_str(),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
Ok(authors
|
||||
.into_iter()
|
||||
.map(|member| {
|
||||
let mut user = member.user;
|
||||
user.url = Some(format!("https://modrinth.com/user/{}", user.id));
|
||||
user.role = member.role;
|
||||
user
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// TODO: modrinth api v3
|
||||
// pub async fn get_org_projects(organization: &str) -> Result<>
|
||||
|
||||
pub async fn get_all_versions(
|
||||
project_id: &str,
|
||||
game_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
page: Option<u32>,
|
||||
page_size: Option<u16>,
|
||||
) -> Result<(Vec<ModrinthVersion>, Pagination)> {
|
||||
let mut url = url::Url::parse(format_url!("/project/{}/version", project_id).as_str())?;
|
||||
if let Some(game_versions) = game_versions {
|
||||
url.query_pairs_mut().append_pair(
|
||||
"game_versions",
|
||||
&game_versions
|
||||
.iter()
|
||||
.map(|v| format!("\"{v}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(","),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(loaders) = loaders {
|
||||
url.query_pairs_mut().append_pair(
|
||||
"loaders",
|
||||
format!(
|
||||
"[{}]",
|
||||
&loaders
|
||||
.iter()
|
||||
.map(|l| format!("\"{l}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
let data = serde_json::from_slice::<Vec<ModrinthVersion>>(
|
||||
&fetch(
|
||||
format_url!("/project/{}/version", project_id).as_str(),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
let data_len = data.len() as u32;
|
||||
|
||||
Ok((
|
||||
data,
|
||||
Pagination {
|
||||
index: page.unwrap_or(1),
|
||||
page_size: page_size.unwrap_or(10),
|
||||
result_count: data_len,
|
||||
total_count: data_len,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_versions(versions: Vec<String>) -> Result<Vec<ModrinthVersion>> {
|
||||
Ok(serde_json::from_slice(
|
||||
&fetch(
|
||||
format_url!(
|
||||
"/versions?ids=[{}]",
|
||||
versions
|
||||
.iter()
|
||||
.map(|v| format!("\"{v}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
.as_str(),
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub async fn get_versions_by_hashes(hashes: Vec<String>) -> Result<HashMap<String, ModrinthVersion>> {
|
||||
let body = serde_json::json!({
|
||||
"hashes": hashes,
|
||||
"algorithm": "sha1"
|
||||
});
|
||||
|
||||
Ok(serde_json::from_slice(
|
||||
&fetch_advanced(
|
||||
Method::POST,
|
||||
format_url!("/version_files").as_str(),
|
||||
None,
|
||||
Some(body),
|
||||
None,
|
||||
None,
|
||||
&State::get().await?.fetch_semaphore,
|
||||
)
|
||||
.await?
|
||||
)?)
|
||||
}
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tokio::sync::{OnceCell, RwLock};
|
||||
|
||||
use crate::{data::{Loader, ManagedPackage, ManagedUser, ManagedVersion}, store::{ManagedVersionFile, ProviderSearchResults, SearchResult}, utils::{http, pagination::Pagination}, Result, State};
|
||||
|
||||
async fn fetch<T: DeserializeOwned>(url: &str) -> Result<T> {
|
||||
let state = State::get().await?;
|
||||
Ok(serde_json::from_slice(
|
||||
&http::fetch(
|
||||
format!("{}/{}", crate::constants::SKYCLIENT_BASE_URL, url).as_str(),
|
||||
None,
|
||||
&state.fetch_semaphore
|
||||
).await?
|
||||
)?)
|
||||
}
|
||||
|
||||
static SKYCLIENTSTORE_STATIC: OnceCell<RwLock<SkyClientStore>> = OnceCell::const_new();
|
||||
|
||||
struct SkyClientStore {
|
||||
pub mods: Option<Vec<SkyClientMod>>
|
||||
}
|
||||
|
||||
impl SkyClientStore {
|
||||
pub async fn get() -> Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
|
||||
Ok(Arc::new(
|
||||
SKYCLIENTSTORE_STATIC
|
||||
.get_or_try_init(Self::initialize)
|
||||
.await?
|
||||
.read()
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
async fn initialize() -> Result<RwLock<Self>> {
|
||||
let mods: Vec<SkyClientMod> = fetch("/mods/mods.json").await?;
|
||||
|
||||
Ok(RwLock::new(SkyClientStore {
|
||||
mods: Some(mods)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SkyClientMod {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub nicknames: Vec<String>,
|
||||
pub display: String,
|
||||
pub creator: String,
|
||||
pub discord_code: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
#[serde(default)]
|
||||
pub links: Vec<SkyClientModLink>,
|
||||
pub versions: Vec<SkyClientModVersion>,
|
||||
#[serde(default)]
|
||||
pub commands: Vec<String>,
|
||||
pub icon_scaling: Option<String>,
|
||||
pub oneconfig: Option<bool>,
|
||||
}
|
||||
|
||||
impl Into<SearchResult> for SkyClientMod {
|
||||
fn into(self) -> SearchResult {
|
||||
SearchResult {
|
||||
slug: self.id.clone(),
|
||||
project_id: self.id,
|
||||
author: self.creator,
|
||||
categories: vec![],
|
||||
title: self.display,
|
||||
client_side: crate::store::PackageSide::Required,
|
||||
server_side: crate::store::PackageSide::Unknown,
|
||||
date_created: None,
|
||||
date_modified: None,
|
||||
description: self.description.unwrap_or_default(),
|
||||
project_type: crate::data::PackageType::Mod,
|
||||
downloads: 0,
|
||||
icon_url: self.icon.map(|i| format!("{}/icons/{}", crate::constants::SKYCLIENT_BASE_URL, i)).unwrap_or_default(),
|
||||
versions: self.versions.iter().map(|v| v.version.clone()).collect(),
|
||||
follows: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<ManagedPackage> for SkyClientMod {
|
||||
fn into(self) -> ManagedPackage {
|
||||
ManagedPackage {
|
||||
id: self.id.clone(),
|
||||
main: self.id,
|
||||
title: self.display,
|
||||
body: crate::store::PackageBody::Markdown(self.description.clone().unwrap_or_default()),
|
||||
categories: vec![],
|
||||
client: crate::store::PackageSide::Required,
|
||||
server: crate::store::PackageSide::Unknown,
|
||||
created: None, // TODO: Get the oldest date from versions
|
||||
updated: None, // TODO: Get the newest date from versions
|
||||
description: self.description.unwrap_or_default(),
|
||||
downloads: 0,
|
||||
followers: 0,
|
||||
game_versions: self.versions.iter().flat_map(|v| v.game_versions.clone()).collect(),
|
||||
icon_url: self.icon.map(|i| format!("{}/icons/{}", crate::constants::SKYCLIENT_BASE_URL, i)),
|
||||
is_archived: false,
|
||||
license: None,
|
||||
loaders: self.versions.iter().flat_map(|v| v.loaders.clone().into_iter().map(Loader::from_string)).collect(),
|
||||
versions: self.versions.iter().map(|v| v.version.clone()).collect(),
|
||||
optional_categories: None,
|
||||
author: crate::store::Author::Users(vec![
|
||||
ManagedUser {
|
||||
id: self.creator.clone(),
|
||||
username: self.creator,
|
||||
is_organization_user: false,
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
role: None,
|
||||
url: self.discord_code.map(|code| format!("https://discord.gg/{}", code)),
|
||||
}
|
||||
]),
|
||||
package_type: crate::data::PackageType::Mod,
|
||||
provider: super::Providers::SkyClient,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SkyClientModLink {
|
||||
pub icon: String,
|
||||
pub text: String,
|
||||
pub link: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SkyClientModVersion {
|
||||
pub version: String,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub file: String,
|
||||
pub url: String,
|
||||
pub hash: String,
|
||||
pub date_published: DateTime<Utc>,
|
||||
pub mod_id: Option<String>,
|
||||
}
|
||||
|
||||
fn get_managed_version(m: &SkyClientMod, v: &SkyClientModVersion) -> ManagedVersion {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(String::from("md5"), v.hash.clone());
|
||||
|
||||
ManagedVersion {
|
||||
id: v.version.clone(),
|
||||
package_id: m.id.clone(),
|
||||
author: m.creator.clone(),
|
||||
changelog_url: None,
|
||||
changelog: String::new(),
|
||||
deps: vec![],
|
||||
downloads: 0,
|
||||
featured: false,
|
||||
files: vec![ManagedVersionFile {
|
||||
file_name: v.file.clone(),
|
||||
url: v.url.clone(),
|
||||
file_type: None,
|
||||
hashes: map,
|
||||
primary: true,
|
||||
size: 0,
|
||||
}],
|
||||
game_versions: v.game_versions.clone(),
|
||||
is_available: true,
|
||||
loaders: v.loaders.iter().map(|l| Loader::from_string(l.to_owned())).collect(),
|
||||
name: format!("{} v{}", m.display, v.version),
|
||||
published: Some(v.date_published),
|
||||
version_display: v.version.clone(),
|
||||
version_type: crate::store::ManagedVersionReleaseType::Release,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(id: &str) -> Result<SkyClientMod> {
|
||||
let store = SkyClientStore::get().await?;
|
||||
|
||||
let Some(mods) = &store.mods else {
|
||||
return Err(anyhow::anyhow!("mod not found").into())
|
||||
};
|
||||
|
||||
for m in mods {
|
||||
if m.id == id {
|
||||
return Ok(m.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("mod not found").into())
|
||||
}
|
||||
|
||||
pub async fn get_multiple(slug_or_ids: &[String]) -> Result<Vec<SkyClientMod>> {
|
||||
let store = SkyClientStore::get().await?;
|
||||
|
||||
let mods = match &store.mods {
|
||||
Some(mods) => mods,
|
||||
None => return Ok(vec![])
|
||||
};
|
||||
|
||||
let mut results = vec![];
|
||||
|
||||
for id in slug_or_ids {
|
||||
for m in mods {
|
||||
if m.id == *id {
|
||||
results.push(m.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn get_all_versions(
|
||||
project_id: &str,
|
||||
game_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
page: Option<u32>,
|
||||
page_size: Option<u16>,
|
||||
) -> Result<(Vec<ManagedVersion>, Pagination)> {
|
||||
let mut pagination = Pagination::default();
|
||||
let mut versions = vec![];
|
||||
|
||||
if let Ok(m) = &get(project_id).await {
|
||||
for v in &m.versions {
|
||||
let mut can_add = true;
|
||||
|
||||
if let Some(game_versions) = &game_versions {
|
||||
if !game_versions.is_empty() {
|
||||
can_add = v.game_versions.iter().any(|gv| game_versions.contains(gv))
|
||||
}
|
||||
}
|
||||
|
||||
if can_add {
|
||||
if let Some(loaders) = &loaders {
|
||||
if !loaders.is_empty() {
|
||||
can_add = v.loaders.iter().any(|l| loaders.contains(&Loader::from_string(l.to_owned())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_add {
|
||||
if page_size.map(|ps| versions.len() < ps as usize).unwrap_or(true) && page.map(|p| p <= pagination.index).unwrap_or(true) {
|
||||
versions.push(get_managed_version(m, v));
|
||||
}
|
||||
|
||||
pagination.total_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((versions, pagination))
|
||||
}
|
||||
|
||||
pub async fn get_versions(versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
|
||||
let store = SkyClientStore::get().await?;
|
||||
|
||||
let mods = match &store.mods {
|
||||
Some(mods) => mods,
|
||||
None => return Ok(vec![])
|
||||
};
|
||||
|
||||
let mut results = vec![];
|
||||
|
||||
for m in mods {
|
||||
for v in &m.versions {
|
||||
if versions.contains(&v.version) {
|
||||
results.push(get_managed_version(m, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn get_versions_by_hashes(hashes: Vec<String>) -> Result<HashMap<String, ManagedVersion>> {
|
||||
let store = SkyClientStore::get().await?;
|
||||
|
||||
let mods = match &store.mods {
|
||||
Some(mods) => mods,
|
||||
None => return Ok(HashMap::new())
|
||||
};
|
||||
|
||||
let mut results = HashMap::new();
|
||||
|
||||
for m in mods {
|
||||
for v in &m.versions {
|
||||
if hashes.contains(&v.hash) {
|
||||
results.insert(v.hash.clone(), get_managed_version(m, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
query: Option<String>,
|
||||
limit: Option<u8>,
|
||||
offset: Option<u32>,
|
||||
game_versions: Option<Vec<String>>,
|
||||
// package_types: Option<Vec<PackageType>>,
|
||||
loaders: Option<Vec<Loader>>,
|
||||
) -> Result<ProviderSearchResults> {
|
||||
let store = SkyClientStore::get().await?;
|
||||
|
||||
let mut results = ProviderSearchResults {
|
||||
provider: super::Providers::SkyClient,
|
||||
results: vec![],
|
||||
total: 0,
|
||||
};
|
||||
|
||||
let mods = match &store.mods {
|
||||
Some(mods) => mods,
|
||||
None => return Ok(results),
|
||||
};
|
||||
|
||||
let query = query.map(|q| q.trim().to_lowercase());
|
||||
let limit = limit.map(|l| l as usize);
|
||||
let offset = offset.unwrap_or(0);
|
||||
|
||||
for m in mods {
|
||||
let mut can_add = true;
|
||||
|
||||
if let Some(query) = &query {
|
||||
if !query.is_empty() {
|
||||
let display = m.display.to_lowercase();
|
||||
|
||||
can_add = display.contains(query) || m.id.contains(query) || m.nicknames.contains(query)
|
||||
}
|
||||
}
|
||||
|
||||
if can_add {
|
||||
if let Some(game_versions) = &game_versions {
|
||||
if !game_versions.is_empty() {
|
||||
can_add = m.versions.iter().any(|v| v.game_versions.iter().any(|gv| game_versions.contains(&gv)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_add {
|
||||
if let Some(loaders) = &loaders {
|
||||
if !loaders.is_empty() {
|
||||
can_add = m.versions.iter().any(|v| v.loaders.iter().any(|l| loaders.contains(&Loader::from_string(l.to_owned()))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_add {
|
||||
if limit.map(|l| results.results.len() < l).unwrap_or(true) && offset <= results.total {
|
||||
let result: SearchResult = m.clone().into();
|
||||
results.results.push(result);
|
||||
}
|
||||
|
||||
results.total += 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
|
@ -1,409 +0,0 @@
|
|||
//! Install modpacks from different sources.
|
||||
|
||||
use crate::data::{Loader, ManagedPackage, ManagedVersion, PackageData};
|
||||
use crate::prelude::ClusterPath;
|
||||
use crate::proxy::send::{init_ingress, send_ingress};
|
||||
use crate::proxy::IngressId;
|
||||
use crate::store::{ClusterStage, PackageSide};
|
||||
use crate::utils::http::{fetch, fetch_advanced, fetch_json, write_icon};
|
||||
use crate::{IngressType, InnerPathLinux, State};
|
||||
use onelauncher_utils::io;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFormat {
|
||||
pub game: String,
|
||||
pub format_version: i32,
|
||||
pub version_id: String,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub files: Vec<PackFile>,
|
||||
pub dependencies: HashMap<PackDependency, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFile {
|
||||
pub path: InnerPathLinux,
|
||||
pub hashes: HashMap<PackFileHash, String>,
|
||||
pub env: Option<HashMap<EnvType, PackageSide>>,
|
||||
pub downloads: Vec<String>,
|
||||
pub file_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
#[serde(rename_all = "camelCase", from = "String")]
|
||||
pub enum PackFileHash {
|
||||
Sha1,
|
||||
Sha512,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for PackFileHash {
|
||||
fn from(s: String) -> Self {
|
||||
match s.as_str() {
|
||||
"sha1" => Self::Sha1,
|
||||
"sha512" => Self::Sha512,
|
||||
_ => Self::Unknown(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EnvType {
|
||||
Client,
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
pub enum PackDependency {
|
||||
#[serde(rename = "forge")]
|
||||
Forge,
|
||||
|
||||
#[serde(rename = "neoforge")]
|
||||
#[serde(alias = "neo-forge")]
|
||||
NeoForge,
|
||||
|
||||
#[serde(rename = "fabric-loader")]
|
||||
FabricLoader,
|
||||
|
||||
#[serde(rename = "quilt-loader")]
|
||||
QuiltLoader,
|
||||
|
||||
#[serde(rename = "minecraft")]
|
||||
Minecraft,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum CreatePackLocation {
|
||||
/// Create a pack from a Modrinth modpack.
|
||||
FromModrinth {
|
||||
package_id: String,
|
||||
version_id: String,
|
||||
title: String,
|
||||
icon_url: Option<String>,
|
||||
},
|
||||
/// Create a pack from a file (importing, importing an mrpack or zip-style modpack)
|
||||
FromFile { path: PathBuf },
|
||||
}
|
||||
|
||||
/// Modpack associated [`Cluster`] wrapper.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePackCluster {
|
||||
/// The name of the cluster, and relative path.
|
||||
pub name: String,
|
||||
/// The game version (MC) of the cluster.
|
||||
pub mc_version: String,
|
||||
/// The mod [`Loader`] to use (if applicable).
|
||||
pub mod_loader: Loader,
|
||||
/// The mod [`Loader`] version to use, set to `latest` or `stable` or the ID of the loader.
|
||||
/// The default value is `latest` for the latest version of the mod loader.
|
||||
pub loader_version: Option<String>,
|
||||
/// The icon of the cluster.
|
||||
pub icon: Option<PathBuf>,
|
||||
/// The URL of an icon for a cluster. (ONLY USE THIS FOR A TEMPORARY CLUSTER)
|
||||
pub icon_url: Option<String>,
|
||||
/// The linked package data (mainly used for modpacks and updating files).
|
||||
pub package_data: Option<PackageData>,
|
||||
/// Whether or not the cluster should skip being installed.
|
||||
pub skip: Option<bool>,
|
||||
/// Whether or not the cluster should skip file watching.
|
||||
pub skip_watch: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for CreatePackCluster {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Untitled".to_string(),
|
||||
mc_version: "1.20.4".to_string(),
|
||||
mod_loader: Loader::Vanilla,
|
||||
loader_version: None,
|
||||
icon: None,
|
||||
icon_url: None,
|
||||
package_data: None,
|
||||
skip: Some(true),
|
||||
skip_watch: Some(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CreatePack {
|
||||
pub file: bytes::Bytes,
|
||||
pub description: CreatePackDescription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CreatePackDescription {
|
||||
pub icon: Option<PathBuf>,
|
||||
pub override_title: Option<String>,
|
||||
pub package_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub existing_ingress: Option<IngressId>,
|
||||
pub cluster_path: ClusterPath,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_cluster_from_pack(location: CreatePackLocation) -> CreatePackCluster {
|
||||
match location {
|
||||
CreatePackLocation::FromModrinth {
|
||||
package_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
} => CreatePackCluster {
|
||||
name: title,
|
||||
icon_url,
|
||||
package_data: Some(PackageData {
|
||||
package_id: Some(package_id),
|
||||
version_id: Some(version_id),
|
||||
locked: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
CreatePackLocation::FromFile { path } => {
|
||||
let file_name = path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
CreatePackCluster {
|
||||
name: file_name,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn generate_pack_from_version_id(
|
||||
package_id: String,
|
||||
version_id: String,
|
||||
title: String,
|
||||
icon_url: Option<String>,
|
||||
cluster_path: ClusterPath,
|
||||
existing_ingress: Option<IngressId>,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let ingress = if let Some(ing) = existing_ingress {
|
||||
send_ingress(&ing, 0.0, Some("downloading pack file")).await?;
|
||||
ing
|
||||
} else {
|
||||
init_ingress(
|
||||
IngressType::DownloadPack {
|
||||
cluster_path: cluster_path.full_path().await?,
|
||||
package_name: title,
|
||||
icon: icon_url,
|
||||
package_version: version_id.clone(),
|
||||
},
|
||||
100.0,
|
||||
"downloading pack file",
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
send_ingress(&ingress, 0.0, Some("fetching version")).await?;
|
||||
let version: ManagedVersion = fetch_json(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"{}version/{}",
|
||||
crate::constants::MODRINTH_API_URL,
|
||||
version_id
|
||||
),
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
send_ingress(&ingress, 10.0, None).await?;
|
||||
|
||||
let (url, hash) = if let Some(file) = version.files.iter().find(|x| x.primary) {
|
||||
Some((file.url.clone(), file.hashes.get("sha1")))
|
||||
} else {
|
||||
version
|
||||
.files
|
||||
.first()
|
||||
.map(|file| (file.url.clone(), file.hashes.get("sha1")))
|
||||
}
|
||||
.ok_or_else(|| anyhow::anyhow!("specified version has no files"))?;
|
||||
|
||||
let file = fetch_advanced(
|
||||
Method::GET,
|
||||
&url,
|
||||
hash.map(|x| &**x),
|
||||
None,
|
||||
None,
|
||||
Some((&ingress, 70.0)),
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
send_ingress(&ingress, 0.0, Some("fetching project metadata")).await?;
|
||||
|
||||
let pkg: ManagedPackage = fetch_json(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"{}project/{}",
|
||||
crate::constants::MODRINTH_API_URL,
|
||||
version.package_id
|
||||
),
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_ingress(&ingress, 10.0, Some("Retrieving icon")).await?;
|
||||
let icon = if let Some(icon_url) = pkg.icon_url {
|
||||
let state = State::get().await?;
|
||||
let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?;
|
||||
let filename = icon_url.rsplit('/').next();
|
||||
|
||||
if let Some(filename) = filename {
|
||||
Some(
|
||||
write_icon(
|
||||
filename,
|
||||
&state.directories.caches_dir().await,
|
||||
icon_bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
send_ingress(&ingress, 10.0, None).await?;
|
||||
|
||||
Ok(CreatePack {
|
||||
file,
|
||||
description: CreatePackDescription {
|
||||
icon,
|
||||
override_title: None,
|
||||
package_id: Some(package_id),
|
||||
version_id: Some(version_id),
|
||||
existing_ingress: Some(ingress),
|
||||
cluster_path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn generate_pack_from_file(
|
||||
path: PathBuf,
|
||||
cluster_path: ClusterPath,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let file = io::read(&path).await?;
|
||||
Ok(CreatePack {
|
||||
file: bytes::Bytes::from(file),
|
||||
description: CreatePackDescription {
|
||||
icon: None,
|
||||
override_title: None,
|
||||
package_id: None,
|
||||
version_id: None,
|
||||
existing_ingress: None,
|
||||
cluster_path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets generated cluster attributes to the pack attributes (using `cluster::edit`).
|
||||
/// This includes the pack name, icon, game version, loader version, and loader.
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn set_cluster_information<S: ::std::hash::BuildHasher>(
|
||||
cluster_path: ClusterPath,
|
||||
description: &CreatePackDescription,
|
||||
backup_name: &str,
|
||||
deps: &HashMap<PackDependency, String, S>,
|
||||
ignore_lock: bool,
|
||||
) -> crate::Result<()> {
|
||||
let mut game_version: Option<&String> = None;
|
||||
let mut mod_loader = None;
|
||||
let mut loader_version = None;
|
||||
|
||||
for (key, value) in deps {
|
||||
match key {
|
||||
PackDependency::Forge => {
|
||||
mod_loader = Some(Loader::Forge);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::NeoForge => {
|
||||
mod_loader = Some(Loader::NeoForge);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::FabricLoader => {
|
||||
mod_loader = Some(Loader::Fabric);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::QuiltLoader => {
|
||||
mod_loader = Some(Loader::Quilt);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::Minecraft => game_version = Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
let Some(game_version) = game_version else {
|
||||
return Err(anyhow::anyhow!("pack did not specify Minecraft version").into());
|
||||
};
|
||||
let mod_loader = mod_loader.unwrap_or(Loader::Vanilla);
|
||||
let loader_version = if mod_loader == Loader::Vanilla {
|
||||
None
|
||||
} else {
|
||||
crate::cluster::create::get_loader_version(
|
||||
game_version.clone(),
|
||||
mod_loader,
|
||||
loader_version.cloned(),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
// crate::api::cluster::create::create_cluster(
|
||||
// description.override_title.clone().unwrap_or_else(|| backup_name.to_string()),
|
||||
// mc_version, mod_loader, loader_version, icon, icon_url, package_data, skip, skip_watch)
|
||||
|
||||
crate::api::cluster::edit(&cluster_path, |cl| {
|
||||
cl.meta.name = description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
cl.stage = ClusterStage::PackDownloading;
|
||||
let package_id = description.package_id.clone();
|
||||
let version_id = description.version_id.clone();
|
||||
|
||||
cl.meta.package_data = if package_id.is_some() && version_id.is_some() {
|
||||
Some(PackageData {
|
||||
package_id,
|
||||
version_id,
|
||||
locked: if ignore_lock {
|
||||
cl.meta.package_data.as_ref().and_then(|x| x.locked)
|
||||
} else {
|
||||
Some(true)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cl.meta.icon.clone_from(&description.icon);
|
||||
cl.meta.mc_version.clone_from(game_version);
|
||||
cl.meta.loader_version.clone_from(&loader_version);
|
||||
cl.meta.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
//! Launcher Import: `ATLauncher`
|
||||
//! Source Code available at <https://github.com/ATLauncher/ATLauncher>
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::package::from::CreatePackDescription;
|
||||
use crate::package::import::{self, copy_minecraft};
|
||||
use crate::package::{self};
|
||||
use crate::prelude::{Cluster, ClusterPath, Loader};
|
||||
use crate::store::{ClusterStage, PackageData, State};
|
||||
use onelauncher_utils::io;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATInstance {
|
||||
pub id: String, // minecraft version id ie: 1.12.1, not a name
|
||||
pub launcher: ATLauncher,
|
||||
pub java_version: ATJavaVersion,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATLauncher {
|
||||
pub name: String,
|
||||
pub pack: String,
|
||||
pub version: String,
|
||||
pub loader_version: ATLauncherLoaderVersion,
|
||||
|
||||
pub managed_project: Option<ATLauncherManagedProject>,
|
||||
pub managed_version: Option<ATLauncherManagedVersion>,
|
||||
pub managed_manifest: Option<package::from::PackFormat>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATJavaVersion {
|
||||
pub major_version: u8,
|
||||
pub component: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATLauncherLoaderVersion {
|
||||
pub r#type: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ATLauncherManagedProject {
|
||||
pub id: String,
|
||||
pub slug: String,
|
||||
pub project_type: String,
|
||||
pub team: String,
|
||||
pub client_side: Option<String>,
|
||||
pub server_side: Option<String>,
|
||||
pub categories: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ATLauncherManagedVersion {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATLauncherManagedVersionFile {
|
||||
pub hashes: HashMap<String, String>,
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub primary: bool,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATLauncherManagedVersionDependency {
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATLauncherMod {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub file: String,
|
||||
|
||||
pub managed_project: Option<ATLauncherManagedProject>,
|
||||
pub managed_version: Option<ATLauncherManagedVersion>,
|
||||
}
|
||||
|
||||
pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
|
||||
let instance: String = io::read_to_string(&instance_folder.join("instance.json"))
|
||||
.await
|
||||
.unwrap_or(String::new());
|
||||
let instance: Result<ATInstance, serde_json::Error> =
|
||||
serde_json::from_str::<ATInstance>(&instance);
|
||||
if let Err(e) = instance {
|
||||
tracing::warn!(
|
||||
"failed to parse instance.json at {}: {}",
|
||||
instance_folder.display(),
|
||||
e
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn import_atlauncher(
|
||||
// The path to the base ATLauncher folder.
|
||||
atlauncher_base_path: PathBuf,
|
||||
// The instance folder in atlauncher_base_path
|
||||
instance_folder: String,
|
||||
// The path to the importing Cluster
|
||||
cluster_path: ClusterPath,
|
||||
) -> crate::Result<()> {
|
||||
let atlauncher_instance_path = atlauncher_base_path
|
||||
.join("instances")
|
||||
.join(instance_folder.clone());
|
||||
let atinstance: String =
|
||||
io::read_to_string(&atlauncher_instance_path.join("instance.json")).await?;
|
||||
let atinstance: ATInstance = serde_json::from_str::<ATInstance>(&atinstance)?;
|
||||
|
||||
// Icon path should be {instance_folder}/instance.png if it exists,
|
||||
// Another possibility is ATLauncher/configs/images/{safe_pack_name}.png (safe pack name is alphanumeric lowercase)
|
||||
let icon_path_primary = atlauncher_instance_path.join("instance.png");
|
||||
let safe_pack_name = atinstance
|
||||
.launcher
|
||||
.pack
|
||||
.replace(|c: char| !c.is_alphanumeric(), "")
|
||||
.to_lowercase();
|
||||
let icon_path_secondary = atlauncher_base_path
|
||||
.join("configs")
|
||||
.join("images")
|
||||
.join(safe_pack_name + ".png");
|
||||
let icon = match (icon_path_primary.exists(), icon_path_secondary.exists()) {
|
||||
(true, _) => import::cache_icon(icon_path_primary).await?,
|
||||
(_, true) => import::cache_icon(icon_path_secondary).await?,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Create description from instance.cfg
|
||||
let description = CreatePackDescription {
|
||||
icon,
|
||||
override_title: Some(atinstance.launcher.name.clone()),
|
||||
package_id: None,
|
||||
version_id: None,
|
||||
existing_ingress: None,
|
||||
cluster_path: cluster_path.clone(),
|
||||
};
|
||||
|
||||
let backup_name = format!("ATLauncher-{instance_folder}");
|
||||
let minecraft_folder = atlauncher_instance_path;
|
||||
|
||||
import_atlauncher_unmanaged(
|
||||
cluster_path,
|
||||
minecraft_folder,
|
||||
backup_name,
|
||||
description,
|
||||
atinstance,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_atlauncher_unmanaged(
|
||||
cluster_path: ClusterPath,
|
||||
minecraft_folder: PathBuf,
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
atinstance: ATInstance,
|
||||
) -> crate::Result<()> {
|
||||
let mod_loader = format!(
|
||||
"\"{}\"",
|
||||
atinstance.launcher.loader_version.r#type.to_lowercase()
|
||||
);
|
||||
let mod_loader: Loader = serde_json::from_str::<Loader>(&mod_loader)?;
|
||||
|
||||
let game_version = atinstance.id;
|
||||
let loader_version = if mod_loader == Loader::Vanilla {
|
||||
None
|
||||
} else {
|
||||
crate::cluster::create::get_loader_version(
|
||||
game_version.clone(),
|
||||
mod_loader,
|
||||
Some(atinstance.launcher.loader_version.version.clone()),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
crate::api::cluster::edit(&cluster_path, |cl| {
|
||||
cl.meta.name = description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
cl.stage = ClusterStage::PackDownloading;
|
||||
cl.meta.package_data = Some(PackageData {
|
||||
package_id: description.package_id.clone(),
|
||||
version_id: description.version_id.clone(),
|
||||
locked: Some(description.package_id.is_some() && description.version_id.is_some()),
|
||||
});
|
||||
cl.meta.icon.clone_from(&description.icon);
|
||||
cl.meta.mc_version.clone_from(&game_version);
|
||||
cl.meta.loader_version.clone_from(&loader_version);
|
||||
cl.meta.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
let ingress = copy_minecraft(
|
||||
cluster_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
|
||||
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut watcher = state.watcher.write().await;
|
||||
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
//! Launcher Import: `CurseForge`
|
||||
//! Closed Source, all this information is datamined unfortunately.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{cache_icon, copy_minecraft};
|
||||
use crate::prelude::{Cluster, ClusterPath, Loader};
|
||||
use crate::store::{ClusterStage, State};
|
||||
use crate::utils::http::{fetch, write_icon};
|
||||
use onelauncher_utils::io;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MinecraftInstance {
|
||||
pub name: Option<String>,
|
||||
pub base_mod_loader: Option<MinecraftInstanceModLoader>,
|
||||
pub profile_image_path: Option<PathBuf>,
|
||||
pub installed_modpack: Option<InstalledModpack>,
|
||||
/// Minecraft game version. Non-prioritized, use this if Vanilla
|
||||
pub game_version: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MinecraftInstanceModLoader {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstalledModpack {
|
||||
pub thumbnail_url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
|
||||
let minecraftinstance: String =
|
||||
io::read_to_string(&instance_folder.join("minecraftinstance.json"))
|
||||
.await
|
||||
.unwrap_or(String::new());
|
||||
let minecraftinstance: Result<MinecraftInstance, serde_json::Error> =
|
||||
serde_json::from_str::<MinecraftInstance>(&minecraftinstance);
|
||||
minecraftinstance.is_ok()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn import_curseforge(
|
||||
// The path to the CurseForge instance
|
||||
curseforge_instance_folder: PathBuf,
|
||||
// The path to the cluster
|
||||
cluster_path: ClusterPath,
|
||||
) -> crate::Result<()> {
|
||||
let minecraft_instance: String =
|
||||
io::read_to_string(&curseforge_instance_folder.join("minecraftinstance.json")).await?;
|
||||
let minecraft_instance: MinecraftInstance =
|
||||
serde_json::from_str::<MinecraftInstance>(&minecraft_instance)?;
|
||||
let override_title: Option<String> = minecraft_instance.name.clone();
|
||||
let backup_name = format!(
|
||||
"Curseforge-{}",
|
||||
curseforge_instance_folder
|
||||
.file_name()
|
||||
.map_or("Unknown".to_string(), |a| a.to_string_lossy().to_string())
|
||||
);
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut icon = None;
|
||||
|
||||
if let Some(icon_path) = minecraft_instance.profile_image_path.clone() {
|
||||
icon = cache_icon(icon_path).await?;
|
||||
} else if let Some(InstalledModpack {
|
||||
thumbnail_url: Some(thumbnail_url),
|
||||
}) = minecraft_instance.installed_modpack.clone()
|
||||
{
|
||||
let icon_bytes = fetch(&thumbnail_url, None, &state.fetch_semaphore).await?;
|
||||
let filename = thumbnail_url.rsplit('/').last();
|
||||
if let Some(filename) = filename {
|
||||
icon = Some(
|
||||
write_icon(
|
||||
filename,
|
||||
&state.directories.caches_dir().await,
|
||||
icon_bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(instance_mod_loader) = minecraft_instance.base_mod_loader {
|
||||
let game_version = minecraft_instance.game_version;
|
||||
let mut mod_loader = None;
|
||||
let mut loader_version = None;
|
||||
|
||||
match instance_mod_loader.name.split('-').collect::<Vec<&str>>()[..] {
|
||||
["forge", version] => {
|
||||
mod_loader = Some(Loader::Forge);
|
||||
loader_version = Some(version.to_string());
|
||||
}
|
||||
["fabric", version, _game_version] => {
|
||||
mod_loader = Some(Loader::Fabric);
|
||||
loader_version = Some(version.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mod_loader = mod_loader.unwrap_or(Loader::Vanilla);
|
||||
let loader_version = if mod_loader == Loader::Vanilla {
|
||||
None
|
||||
} else {
|
||||
crate::cluster::create::get_loader_version(
|
||||
game_version.clone(),
|
||||
mod_loader,
|
||||
loader_version,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
crate::api::cluster::edit(&cluster_path, |cl| {
|
||||
cl.meta.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
cl.stage = ClusterStage::PackDownloading;
|
||||
cl.meta.icon.clone_from(&icon);
|
||||
cl.meta.mc_version.clone_from(&game_version);
|
||||
cl.meta.loader_version.clone_from(&loader_version);
|
||||
cl.meta.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
crate::api::cluster::edit(&cluster_path, |cl| {
|
||||
cl.meta.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
cl.meta.icon.clone_from(&icon);
|
||||
cl.meta
|
||||
.mc_version
|
||||
.clone_from(&minecraft_instance.game_version);
|
||||
cl.meta.loader_version = None;
|
||||
cl.meta.loader = Loader::Vanilla;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let ingress = copy_minecraft(
|
||||
cluster_path.clone(),
|
||||
curseforge_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
|
||||
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut watcher = state.watcher.write().await;
|
||||
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
//! Launcher Import: FTB App
|
||||
//! Source Code available at <https://github.com/FTBTeam/FTB-App>
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
//! Launcher Import: `GDLauncher` (legacy)
|
||||
//! Source Code available at <https://github.com/gorilla-devs/GDLauncher>
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{cache_icon, copy_minecraft};
|
||||
use crate::prelude::{Cluster, ClusterPath, Loader};
|
||||
use crate::store::{ClusterStage, State};
|
||||
use onelauncher_utils::io;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GDLauncherConfig {
|
||||
pub background: Option<String>,
|
||||
pub loader: GDLauncherLoader,
|
||||
// pub mods: Vec<GDLauncherMod>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GDLauncherLoader {
|
||||
pub loader_type: Loader,
|
||||
pub loader_version: Option<String>,
|
||||
pub mc_version: String,
|
||||
pub source: Option<String>,
|
||||
pub source_name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
|
||||
let config: String = io::read_to_string(&instance_folder.join("config.json"))
|
||||
.await
|
||||
.unwrap_or(String::new());
|
||||
let config: Result<GDLauncherConfig, serde_json::Error> =
|
||||
serde_json::from_str::<GDLauncherConfig>(&config);
|
||||
config.is_ok()
|
||||
}
|
||||
|
||||
pub async fn import_gdlauncher(
|
||||
gdlauncher_instance_folder: PathBuf,
|
||||
cluster_path: ClusterPath,
|
||||
) -> crate::Result<()> {
|
||||
let config: String =
|
||||
io::read_to_string(&gdlauncher_instance_folder.join("config.json")).await?;
|
||||
let config: GDLauncherConfig = serde_json::from_str::<GDLauncherConfig>(&config)?;
|
||||
let override_title: Option<String> = config.loader.source_name.clone();
|
||||
let backup_name = format!(
|
||||
"GDLauncher-{}",
|
||||
gdlauncher_instance_folder
|
||||
.file_name()
|
||||
.map_or("Unknown".to_string(), |a| a.to_string_lossy().to_string())
|
||||
);
|
||||
|
||||
let icon = config
|
||||
.background
|
||||
.clone()
|
||||
.map(|b| gdlauncher_instance_folder.join(b));
|
||||
let icon = if let Some(icon) = icon {
|
||||
cache_icon(icon).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let game_version = config.loader.mc_version;
|
||||
let mod_loader = config.loader.loader_type;
|
||||
let loader_version = config.loader.loader_version;
|
||||
|
||||
let loader_version = if mod_loader == Loader::Vanilla {
|
||||
None
|
||||
} else {
|
||||
crate::cluster::create::get_loader_version(game_version.clone(), mod_loader, loader_version)
|
||||
.await?
|
||||
};
|
||||
|
||||
crate::api::cluster::edit(&cluster_path, |cl| {
|
||||
cl.meta.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
cl.stage = ClusterStage::PackDownloading;
|
||||
cl.meta.icon.clone_from(&icon);
|
||||
cl.meta.mc_version.clone_from(&game_version);
|
||||
cl.meta.loader_version.clone_from(&loader_version);
|
||||
cl.meta.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let ingress = copy_minecraft(
|
||||
cluster_path.clone(),
|
||||
gdlauncher_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
|
||||
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut watcher = state.watcher.write().await;
|
||||
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
//! Manages importing data from other launchers.
|
||||
|
||||
use crate::prelude::ClusterPath;
|
||||
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
|
||||
use crate::proxy::IngressId;
|
||||
|
||||
use crate::store::Clusters;
|
||||
use crate::utils::http::{self, IoSemaphore};
|
||||
use onelauncher_utils::io::{self, IOError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub mod atlauncher;
|
||||
pub mod curseforge;
|
||||
pub mod ftb;
|
||||
pub mod gdlauncher;
|
||||
pub mod modrinth;
|
||||
pub mod multibased;
|
||||
pub mod technic;
|
||||
pub mod tlauncher;
|
||||
|
||||
/// List of launcher types we support importing from.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ImportType {
|
||||
/// `MultiMC` based launchers
|
||||
MultiMC,
|
||||
/// Prism Launcher has its own category because it has different logic (also is objectively better than mmc)
|
||||
PrismLauncher,
|
||||
/// `GDLauncher`
|
||||
GDLauncher,
|
||||
/// Curseforge's launcher
|
||||
Curseforge,
|
||||
/// `ATLauncher`
|
||||
ATLauncher,
|
||||
/// Modrinth app.
|
||||
Modrinth,
|
||||
/// `TLauncher`
|
||||
TLauncher,
|
||||
/// FTB Launcher
|
||||
FTBLauncher,
|
||||
/// Technic
|
||||
Technic,
|
||||
/// Unknown import option (not widely adopted -> probably a custom launcher with a similar file structure to the above)
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl fmt::Display for ImportType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MultiMC => write!(f, "MultiMC"),
|
||||
Self::ATLauncher => write!(f, "ATLauncher"),
|
||||
Self::Curseforge => write!(f, "Curseforge"),
|
||||
Self::GDLauncher => write!(f, "GDLauncher"),
|
||||
Self::Modrinth => write!(f, "Modrinth App"),
|
||||
Self::PrismLauncher => write!(f, "PrismLauncher"),
|
||||
Self::TLauncher => write!(f, "TLauncher"),
|
||||
Self::FTBLauncher => write!(f, "Feed The Beast"),
|
||||
Self::Technic => write!(f, "Technic"),
|
||||
Self::Unknown => write!(f, "Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImportType {
|
||||
pub async fn get_instances_subpath(&self, path: &Path) -> crate::Result<PathBuf> {
|
||||
Ok(path.join(
|
||||
&(match self {
|
||||
Self::GDLauncher | Self::ATLauncher => "instances".to_string(),
|
||||
Self::Curseforge => "Instances".to_string(),
|
||||
Self::MultiMC => {
|
||||
multibased::get_instances_subpath(path.to_path_buf().join("multimc.cfg"))
|
||||
.await
|
||||
.unwrap_or_else(|| "instances".to_string())
|
||||
}
|
||||
Self::PrismLauncher => {
|
||||
multibased::get_instances_subpath(path.to_path_buf().join("prismlauncher.cfg"))
|
||||
.await
|
||||
.unwrap_or_else(|| "instances".to_string())
|
||||
}
|
||||
Self::Modrinth => "profiles".to_string(),
|
||||
Self::TLauncher => "tinstances".to_string(),
|
||||
Self::FTBLauncher => "ftbinstances".to_string(),
|
||||
Self::Technic => "tecinstances".to_string(),
|
||||
Self::Unknown => {
|
||||
return Err(anyhow::anyhow!("launcher type unknown, cant import").into())
|
||||
}
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_launcher_instances(import: ImportType, path: Option<PathBuf>) -> crate::Result<(PathBuf, Vec<String>)> {
|
||||
let base_path = match &path {
|
||||
Some(path) => path,
|
||||
None => &default_launcher_path(import).ok_or_else(|| anyhow::anyhow!("could not get launcher base path for {import}"))?
|
||||
};
|
||||
|
||||
let instances_dir = import.get_instances_subpath(base_path).await?;
|
||||
|
||||
let mut instances = Vec::new();
|
||||
let mut dir = io::read_dir(&instances_dir)
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("invalid {import} launcher path, failed to import."))?;
|
||||
|
||||
while let Some(e) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &instances_dir))?
|
||||
{
|
||||
let path = e.path();
|
||||
if path.is_dir() && is_valid_instance(path.clone(), import).await {
|
||||
let name = path.file_name();
|
||||
if let Some(name) = name {
|
||||
instances.push(name.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((base_path.to_owned(), instances))
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn import_instances(
|
||||
import: ImportType,
|
||||
base_path: PathBuf,
|
||||
instances: Vec<String>,
|
||||
) -> crate::Result<()> {
|
||||
for instance in instances {
|
||||
import_instance(import, base_path.clone(), instance).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn import_instance(
|
||||
import: ImportType,
|
||||
base_path: PathBuf,
|
||||
instance_path: String,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("importing instance from {instance_path}");
|
||||
|
||||
let cluster_path = crate::cluster::create::create_unfinished_cluster(instance_path.clone()).await?;
|
||||
|
||||
let result = match import {
|
||||
ImportType::MultiMC | ImportType::PrismLauncher => {
|
||||
multibased::import_mmc(base_path, instance_path, cluster_path.clone()).await
|
||||
}
|
||||
ImportType::ATLauncher => {
|
||||
atlauncher::import_atlauncher(base_path, instance_path, cluster_path.clone()).await
|
||||
}
|
||||
ImportType::GDLauncher => {
|
||||
gdlauncher::import_gdlauncher(
|
||||
base_path.join("instances").join(instance_path),
|
||||
cluster_path.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportType::Curseforge => {
|
||||
curseforge::import_curseforge(
|
||||
base_path.join("Instances").join(instance_path),
|
||||
cluster_path.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportType::Modrinth => {
|
||||
modrinth::import_modrinth(
|
||||
base_path.join("profiles").join(instance_path),
|
||||
cluster_path.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
// ImportType::TLauncher => tlauncher::import_tlauncher(path.join("instances").join(instance_path), cluster_path.clone()).await,
|
||||
// ImportType::Technic => technic::import_technic(path.join("instances").join(instance_path), cluster_path.clone()).await,
|
||||
// ImportType::FTBLauncher => ftb::import_ftb(path.join("instances").join(instance_path), cluster_path.clone()).await,
|
||||
ImportType::Unknown => Err(anyhow::anyhow!("unknown launcher type").into()),
|
||||
_ => todo!(),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to import modpack: {:?}", e);
|
||||
let _ = crate::api::cluster::remove(&cluster_path).await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::task::spawn(Clusters::update_versions());
|
||||
|
||||
tracing::debug!("completed import of instance.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// returns the default path for a given [`ImportType`].
|
||||
#[must_use]
|
||||
pub fn default_launcher_path(r#type: ImportType) -> Option<PathBuf> {
|
||||
let path = match r#type {
|
||||
ImportType::PrismLauncher => Some(dirs::data_dir()?.join("PrismLauncher")),
|
||||
ImportType::ATLauncher => Some(dirs::data_dir()?.join("ATLauncher")),
|
||||
ImportType::GDLauncher => Some(dirs::data_dir()?.join("gdlauncher_next")),
|
||||
ImportType::Curseforge => Some(dirs::home_dir()?.join("curseforge").join("minecraft")),
|
||||
ImportType::Modrinth => Some(dirs::data_dir()?.join("theseus")),
|
||||
ImportType::FTBLauncher => Some(dirs::data_dir()?.join("FTB")),
|
||||
ImportType::Technic => Some(dirs::data_dir()?.join("Technic")),
|
||||
ImportType::TLauncher => Some(dirs::data_dir()?.join("TLauncher")),
|
||||
ImportType::Unknown | ImportType::MultiMC => None, // MultiMC data is in it's application directory
|
||||
};
|
||||
|
||||
let path = path?;
|
||||
if path.exists() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a [`PathBuf`] is a valid instance for a given [`ImportType`]
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn is_valid_instance(instance_path: PathBuf, r#type: ImportType) -> bool {
|
||||
match r#type {
|
||||
ImportType::MultiMC | ImportType::PrismLauncher => {
|
||||
multibased::is_valid_multibased(instance_path).await
|
||||
}
|
||||
ImportType::ATLauncher => atlauncher::is_valid_atlauncher(instance_path).await,
|
||||
ImportType::GDLauncher => gdlauncher::is_valid_gdlauncher(instance_path).await,
|
||||
ImportType::Curseforge => curseforge::is_valid_curseforge(instance_path).await,
|
||||
// ImportType::Modrinth => modrinth::is_valid_modrinth(instance_path).await,
|
||||
// ImportType::TLauncher => tlauncher::is_valid_tlauncher(instance_path).await,
|
||||
// ImportType::FTBLauncher => ftb::is_valid_ftb(instance_path).await,
|
||||
// ImportType::Technic => technic::is_valid_technic(instance_path).await,
|
||||
ImportType::Unknown => false,
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn cache_icon(icon_path: PathBuf) -> crate::Result<Option<PathBuf>> {
|
||||
let state = crate::State::get().await?;
|
||||
let bytes = tokio::fs::read(&icon_path).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
let bytes = bytes::Bytes::from(bytes);
|
||||
let cache_dir = &state.directories.caches_dir().await;
|
||||
let semaphore = &state.io_semaphore;
|
||||
Ok(Some(
|
||||
http::write_icon(&icon_path.to_string_lossy(), cache_dir, bytes, semaphore).await?,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn copy_minecraft(
|
||||
cluster_path: ClusterPath,
|
||||
minecraft_path: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
old_ingress: Option<IngressId>,
|
||||
) -> crate::Result<IngressId> {
|
||||
let cluster_path_full = cluster_path.full_path().await?;
|
||||
let subfiles = sub(&minecraft_path, false).await?;
|
||||
let total_subfiles = subfiles.len() as u64;
|
||||
let ingress = init_or_edit_ingress(
|
||||
old_ingress,
|
||||
crate::IngressType::CopyCluster {
|
||||
import: minecraft_path.clone(),
|
||||
cluster_name: cluster_path.to_string(),
|
||||
},
|
||||
total_subfiles as f64,
|
||||
"copying files in preexisting cluster",
|
||||
)
|
||||
.await?;
|
||||
|
||||
for sub in subfiles {
|
||||
let child = sub
|
||||
.strip_prefix(&minecraft_path)
|
||||
.map_err(|_| anyhow::anyhow!("invalid .minecraft file {}", &sub.display()))?;
|
||||
let child = cluster_path_full.join(child);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
http::copy(&sub, &child, io_semaphore).await?;
|
||||
send_ingress(&ingress, 1.0, None).await?;
|
||||
}
|
||||
|
||||
Ok(ingress)
|
||||
}
|
||||
|
||||
/// recursively get a [`Vec<PathBuf>`] of all subfiles.
|
||||
#[onelauncher_macros::memory]
|
||||
#[async_recursion::async_recursion]
|
||||
#[tracing::instrument]
|
||||
pub async fn sub(path: &Path, include_empty: bool) -> crate::Result<Vec<PathBuf>> {
|
||||
if !path.is_dir() {
|
||||
return Ok(vec![path.to_path_buf()]);
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
let mut dir = io::read_dir(&path).await?;
|
||||
let mut has_files = false;
|
||||
while let Some(child) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, path))?
|
||||
{
|
||||
has_files = true;
|
||||
let path_child = child.path();
|
||||
files.append(&mut sub(&path_child, include_empty).await?);
|
||||
}
|
||||
|
||||
if !has_files && include_empty {
|
||||
files.push(path.to_path_buf());
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
//! Launcher Import: Modrinth App
|
||||
//! Source Code available at <https://github.com/modrinth/code>
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::prelude::ClusterPath;
|
||||
use onelauncher_utils::io;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ModrinthProfile {}
|
||||
|
||||
pub async fn is_valid_modrinth(instance_folder: PathBuf) -> bool {
|
||||
let config: String = io::read_to_string(&instance_folder.join("profile.json"))
|
||||
.await
|
||||
.unwrap_or(String::new());
|
||||
let config: Result<ModrinthProfile, serde_json::Error> =
|
||||
serde_json::from_str::<ModrinthProfile>(&config);
|
||||
config.is_ok()
|
||||
}
|
||||
|
||||
pub async fn import_modrinth(
|
||||
_modrinth_instance_folder: PathBuf,
|
||||
_cluster_path: ClusterPath,
|
||||
) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
//! Launcher Import: `MultiMC` & Prism Launcher
|
||||
//! Source Code available at <https://github.com/PrismLauncher/PrismLauncher>
|
||||
//! Source Code available at <https://github.com/MultiMC/Launcher>
|
||||
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::package::from::{self, CreatePackDescription, PackDependency};
|
||||
use crate::package::import::{self, copy_minecraft};
|
||||
use crate::prelude::{Cluster, ClusterPath, State};
|
||||
use onelauncher_utils::io;
|
||||
|
||||
// instance.cfg: https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/MinecraftInstance.cpp
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[serde(untagged)]
|
||||
enum MMCInstanceEnum {
|
||||
General(MMCInstanceGeneral),
|
||||
Instance(MMCInstance),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct MMCInstanceGeneral {
|
||||
pub general: MMCInstance,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct MMCInstance {
|
||||
pub java_path: Option<String>,
|
||||
pub jvm_args: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_bool")]
|
||||
pub managed_pack: Option<bool>,
|
||||
|
||||
#[serde(rename = "ManagedPackID")]
|
||||
pub managed_pack_id: Option<String>,
|
||||
pub managed_pack_type: Option<MMCManagedPackType>,
|
||||
#[serde(rename = "ManagedPackVersionID")]
|
||||
pub managed_pack_version_id: Option<String>,
|
||||
pub managed_pack_version_name: Option<String>,
|
||||
|
||||
#[serde(rename = "iconKey")]
|
||||
pub icon_key: Option<String>,
|
||||
#[serde(rename = "name")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
/// Deserializes INI String Boolean values into Rust Boolean values for [`serde-ini`].
|
||||
fn deserialize_optional_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
Option::<String>::deserialize(deserializer)?.map_or_else(
|
||||
|| Ok(None),
|
||||
|string| match string.as_str() {
|
||||
"true" => Ok(Some(true)),
|
||||
"false" => Ok(Some(false)),
|
||||
_ => Err(de::Error::custom("expected 'true' or 'false'")),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MMCManagedPackType {
|
||||
Modrinth,
|
||||
Flame,
|
||||
ATLauncher,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
// mmc-pack.json
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MMCPack {
|
||||
components: Vec<MMCComponent>,
|
||||
format_version: u32,
|
||||
}
|
||||
|
||||
// https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/Component.h
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MMCComponent {
|
||||
pub uid: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub version: Option<String>,
|
||||
#[serde(default)]
|
||||
pub dependency_only: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub important: bool,
|
||||
#[serde(default)]
|
||||
pub disabled: bool,
|
||||
|
||||
pub cached_name: Option<String>,
|
||||
pub cached_version: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub cached_requires: Vec<MMCComponentRequirement>,
|
||||
#[serde(default)]
|
||||
pub cached_conflicts: Vec<MMCComponentRequirement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MMCComponentRequirement {
|
||||
pub uid: String,
|
||||
pub equals_version: Option<String>,
|
||||
pub suggests: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[serde(untagged)]
|
||||
enum MMCLauncherEnum {
|
||||
General(MMCLauncherGeneral),
|
||||
Instance(MMCLauncher),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct MMCLauncherGeneral {
|
||||
pub general: MMCLauncher,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct MMCLauncher {
|
||||
instance_dir: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn is_valid_multibased(instance_folder: PathBuf) -> bool {
|
||||
let instance_cfg = instance_folder.join("instance.cfg");
|
||||
let mmc_pack = instance_folder.join("mmc-pack.json");
|
||||
let Ok(mmc_pack) = io::read_to_string(&mmc_pack).await else {
|
||||
return false;
|
||||
};
|
||||
|
||||
load_instance_cfg(&instance_cfg).await.is_ok()
|
||||
&& serde_json::from_str::<MMCPack>(&mmc_pack).is_ok()
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_instances_subpath(config: PathBuf) -> Option<String> {
|
||||
let launcher = io::read_to_string(&config).await.ok()?;
|
||||
let launcher: MMCLauncherEnum = serde_ini::from_str(&launcher).ok()?;
|
||||
match launcher {
|
||||
MMCLauncherEnum::General(p) => Some(p.general.instance_dir),
|
||||
MMCLauncherEnum::Instance(p) => Some(p.instance_dir),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
|
||||
let instance_cfg: String = io::read_to_string(file_path).await?;
|
||||
let instance_cfg_enum: MMCInstanceEnum = serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?;
|
||||
match instance_cfg_enum {
|
||||
MMCInstanceEnum::General(instance_cfg) => Ok(instance_cfg.general),
|
||||
MMCInstanceEnum::Instance(instance_cfg) => Ok(instance_cfg),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn import_mmc(
|
||||
mmc_base_path: PathBuf,
|
||||
instance_folder: String,
|
||||
cluster_path: ClusterPath,
|
||||
) -> crate::Result<()> {
|
||||
let mmc_instance_path = mmc_base_path
|
||||
.join("instances")
|
||||
.join(instance_folder.clone());
|
||||
let mmc_pack = io::read_to_string(&mmc_instance_path.join("mmc-pack.json")).await?;
|
||||
let mmc_pack: MMCPack = serde_json::from_str::<MMCPack>(&mmc_pack)?;
|
||||
let instance_cfg = load_instance_cfg(&mmc_instance_path.join("instance.cfg")).await?;
|
||||
|
||||
let icon = if let Some(icon_key) = instance_cfg.icon_key {
|
||||
let icon_path = mmc_base_path.join("icons").join(icon_key);
|
||||
import::cache_icon(icon_path).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let description = CreatePackDescription {
|
||||
icon,
|
||||
override_title: instance_cfg.name,
|
||||
package_id: instance_cfg.managed_pack_id,
|
||||
version_id: instance_cfg.managed_pack_version_id,
|
||||
existing_ingress: None,
|
||||
cluster_path: cluster_path.clone(),
|
||||
};
|
||||
|
||||
let mut minecraft_folder = mmc_base_path
|
||||
.join("instances")
|
||||
.join(instance_folder);
|
||||
|
||||
if minecraft_folder.join("minecraft").exists() {
|
||||
minecraft_folder = minecraft_folder.join("minecraft");
|
||||
} else if minecraft_folder.join(".minecraft").exists() {
|
||||
minecraft_folder = minecraft_folder.join(".minecraft");
|
||||
}
|
||||
|
||||
let backup_name = if instance_cfg.managed_pack.unwrap_or(false) {
|
||||
match instance_cfg.managed_pack_type {
|
||||
Some(MMCManagedPackType::Modrinth) => "Imported Modrinth Modpack".to_string(),
|
||||
Some(MMCManagedPackType::Flame) => "Imported CurseForge Modpack".to_string(),
|
||||
Some(MMCManagedPackType::ATLauncher) => "Imported ATLauncher Modpack".to_string(),
|
||||
Some(_) => "ImportedModpack".to_string(),
|
||||
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Instance is managed, but managed pack type not specified in instance.cfg"
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"Imported Modpack".to_string()
|
||||
};
|
||||
|
||||
import_mmc_unmanaged(
|
||||
cluster_path,
|
||||
minecraft_folder,
|
||||
backup_name,
|
||||
description,
|
||||
mmc_pack,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_mmc_unmanaged(
|
||||
cluster_path: ClusterPath,
|
||||
minecraft_folder: PathBuf,
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
mmc_pack: MMCPack,
|
||||
) -> crate::Result<()> {
|
||||
let dependencies = mmc_pack
|
||||
.components
|
||||
.iter()
|
||||
.filter_map(|component| {
|
||||
if component.uid.starts_with("net.fabricmc.fabric-loader") {
|
||||
return Some((
|
||||
PackDependency::FabricLoader,
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
if component.uid.starts_with("net.minecraftforge") {
|
||||
return Some((
|
||||
PackDependency::Forge,
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
if component.uid.starts_with("org.quiltmc.quilt-loader") {
|
||||
return Some((
|
||||
PackDependency::QuiltLoader,
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
if component.uid.starts_with("net.minecraft") {
|
||||
return Some((
|
||||
PackDependency::Minecraft,
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
from::set_cluster_information::<std::hash::RandomState>(
|
||||
cluster_path.clone(),
|
||||
&description,
|
||||
&backup_name,
|
||||
&dependencies,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let ingress = copy_minecraft(
|
||||
cluster_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
|
||||
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut watcher = state.watcher.write().await;
|
||||
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
//! Launcher Import: Technic Launcher
|
||||
//! Source Code available at <https://github.com/TechnicPack/LauncherV3>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
//! Launcher Import: `TLauncher`
|
||||
//! Closed source (technically) ;3
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
// //! **OneLauncher Inferral**
|
||||
// //!
|
||||
// //! Infers package metadata beyond just looking up the file hashes.
|
||||
|
||||
// use std::collections::HashMap;
|
||||
// use std::future::Future;
|
||||
// use std::path::PathBuf;
|
||||
// use std::pin::Pin;
|
||||
|
||||
// use async_zip::tokio::read::fs::ZipFileReader;
|
||||
// use serde::Deserialize;
|
||||
|
||||
// use crate::prelude::PackageType;
|
||||
// use crate::store::{read_icon, Package, PackageMetadata};
|
||||
// use crate::utils::http::{FetchSemaphore, IoSemaphore};
|
||||
|
||||
// /// A structure representing a package file that hasn't been analyzed.
|
||||
// pub struct UnanalyzedFile {
|
||||
// path: String,
|
||||
// file_name: String,
|
||||
// package_type: PackageType,
|
||||
// size: u64,
|
||||
// cache: String,
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// #[serde(rename_all = "camelCase")]
|
||||
// /// A structure representing a `jar/META-INF/mods.toml` Forge metadata file.
|
||||
// pub struct ForgeModsInfo {
|
||||
// pub mods: Vec<ForgeMods>,
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// #[serde(rename_all = "camelCase")]
|
||||
// /// A structure representing `jar/META-INF/mods.toml` Forge mod metadata.
|
||||
// pub struct ForgeMods {
|
||||
// mod_id: String,
|
||||
// version: Option<String>,
|
||||
// display_name: Option<String>,
|
||||
// description: Option<String>,
|
||||
// logo_file: Option<String>,
|
||||
// authors: Option<String>,
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// #[serde(rename_all = "camelCase")]
|
||||
// /// A structure representing a `jar/mcmod.info` Forge metadata file.
|
||||
// pub struct ForgeMcMod {
|
||||
// modid: String,
|
||||
// name: String,
|
||||
// description: Option<String>,
|
||||
// version: Option<String>,
|
||||
// author_list: Option<Vec<String>>,
|
||||
// logo_file: Option<String>,
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// #[serde(untagged)]
|
||||
// /// A structure representing `jar/fabric.mod.json` Fabric author metadata.
|
||||
// pub enum FabricAuthor {
|
||||
// String(String),
|
||||
// Object { name: String },
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// #[serde(rename_all = "camelCase")]
|
||||
// /// A structure representing a `jar/fabric.mod.json` Fabric metadata file.
|
||||
// pub struct FabricMod {
|
||||
// id: String,
|
||||
// version: String,
|
||||
// name: Option<String>,
|
||||
// description: Option<String>,
|
||||
// authors: Vec<FabricAuthor>,
|
||||
// icon: Option<String>,
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// /// A structure representing `jar/quilt.mod.json` Quilt metadata.
|
||||
// pub struct QuiltMetadata {
|
||||
// name: Option<String>,
|
||||
// description: Option<String>,
|
||||
// contributors: Option<HashMap<String, String>>,
|
||||
// icon: Option<String>,
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// /// A structure representing a `jar/quilt.mod.json` Quilt metadata file.
|
||||
// pub struct QuiltMod {
|
||||
// id: String,
|
||||
// version: String,
|
||||
// metadata: Option<QuiltMetadata>,
|
||||
// }
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// /// A structure representing a `zip/pack.mcmeta` package metadata file.
|
||||
// pub struct Pack {
|
||||
// description: Option<String>,
|
||||
// }
|
||||
|
||||
// async fn process<F, M>(
|
||||
// zipfr: &ZipFileReader,
|
||||
// disabled: bool,
|
||||
// file_name: String,
|
||||
// sha512: String,
|
||||
// index: usize,
|
||||
// reader_fn: F,
|
||||
// meta_fn: M,
|
||||
// ) -> crate::Result<Package>
|
||||
// where
|
||||
// F: Fn(&ZipFileReader, usize) -> Pin<Box<dyn Future<Output = crate::Result<String>>>>,
|
||||
// M: Fn(String) -> Option<PackageMetadata>,
|
||||
// {
|
||||
// let filestr = reader_fn(zipfr, index).await?;
|
||||
|
||||
// if let Some(meta) = meta_fn(filestr) {
|
||||
// Ok(Package {
|
||||
// sha512,
|
||||
// disabled,
|
||||
// file_name,
|
||||
// meta,
|
||||
// })
|
||||
// } else {
|
||||
// Ok(Package {
|
||||
// sha512,
|
||||
// disabled,
|
||||
// file_name,
|
||||
// meta: PackageMetadata::Unknown,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn forge_metadata(filestr: String) -> Option<PackageMetadata> {
|
||||
// if let Ok(pkg) = toml::from_str::<ForgeModsInfo>(&filestr) {
|
||||
// if let Some(pkg) = pkg.mods.first() {
|
||||
// return Some(PackageMetadata::Mapped {
|
||||
// title: Some(
|
||||
// pkg.display_name
|
||||
// .clone()
|
||||
// .unwrap_or_else(|| pkg.mod_id.clone()),
|
||||
// ),
|
||||
// description: pkg.description.clone(),
|
||||
// authors: pkg.authors.clone().map(|x| vec![x]).unwrap_or_default(),
|
||||
// version: pkg.version.clone(),
|
||||
// icon: pkg.logo_file.clone(),
|
||||
// package_type: Some(PackageType::Mod),
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// None
|
||||
// }
|
||||
|
||||
// pub async fn infer(
|
||||
// hash: String,
|
||||
// path: PathBuf,
|
||||
// cache_dir: PathBuf,
|
||||
// io_semaphore: &IoSemaphore,
|
||||
// fetch_semaphore: &FetchSemaphore,
|
||||
// ) -> crate::Result<Package> {
|
||||
// let file_name = path
|
||||
// .file_name()
|
||||
// .unwrap_or_default()
|
||||
// .to_string_lossy()
|
||||
// .to_string();
|
||||
// let disabled = file_name.ends_with(".disabled");
|
||||
// let zipfr = match ZipFileReader::new(path.clone()).await {
|
||||
// Ok(zipfr) => zipfr,
|
||||
// Err(_) => {
|
||||
// return Ok(Package {
|
||||
// sha512: hash,
|
||||
// disabled,
|
||||
// meta: PackageMetadata::Unknown,
|
||||
// file_name,
|
||||
// })
|
||||
// }
|
||||
// };
|
||||
|
||||
// let zip_idx = zipfr
|
||||
// .file()
|
||||
// .entries()
|
||||
// .iter()
|
||||
// .position(|f| f.filename().as_str().unwrap_or_default() == "META-INF/mods.toml");
|
||||
// if let Some(idx) = zip_idx {
|
||||
// let mut filestr = String::new();
|
||||
// if zipfr
|
||||
// .reader_with_entry(idx)
|
||||
// .await?
|
||||
// .read_to_string_checked(&mut filestr)
|
||||
// .await
|
||||
// .is_ok()
|
||||
// {
|
||||
// if let Ok(pkg) = toml::from_str::<ForgeModsInfo>(&filestr) {
|
||||
// if let Some(pkg) = pkg.mods.first() {
|
||||
// return Ok(Package {
|
||||
// sha512: hash,
|
||||
// disabled,
|
||||
// file_name,
|
||||
// meta: PackageMetadata::Mapped {
|
||||
// title: Some(
|
||||
// pkg.display_name
|
||||
// .clone()
|
||||
// .unwrap_or_else(|| pkg.mod_id.clone()),
|
||||
// ),
|
||||
// description: pkg.description.clone(),
|
||||
// authors: pkg.authors.clone().map(|x| vec![x]).unwrap_or_default(),
|
||||
// version: pkg.version.clone(),
|
||||
// icon: read_icon(pkg.logo_file.clone(), &cache_dir, &path, io_semaphore)
|
||||
// .await?,
|
||||
// package_type: Some(PackageType::Mod),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// let zip_idx = zipfr
|
||||
// .file()
|
||||
// .entries()
|
||||
// .iter()
|
||||
// .position(|f| f.filename().as_str().unwrap_or_default() == "mcmod.info");
|
||||
// if let Some(idx) = zip_idx {
|
||||
// let mut filestr = String::new();
|
||||
// if zipfr
|
||||
// .reader_with_entry(idx)
|
||||
// .await?
|
||||
// .read_to_string_checked(&mut filestr)
|
||||
// .await
|
||||
// .is_ok()
|
||||
// {
|
||||
// if let Ok(pkg) = serde_json::from_str::<ForgeMcMod>(&filestr) {
|
||||
// let icon = read_icon(pkg.logo_file, &cache_dir, &path, io_semaphore).await?;
|
||||
|
||||
// return Ok(Package {
|
||||
// sha512: hash,
|
||||
// disabled,
|
||||
// file_name,
|
||||
// meta: PackageMetadata::Mapped {
|
||||
// title: Some(if pkg.name.is_empty() {
|
||||
// pkg.modid
|
||||
// } else {
|
||||
// pkg.name
|
||||
// }),
|
||||
// description: pkg.description,
|
||||
// authors: pkg.author_list.unwrap_or_default(),
|
||||
// version: pkg.version,
|
||||
// icon,
|
||||
// package_type: Some(PackageType::Mod),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
//! **`OneLauncher` Package**
|
||||
//!
|
||||
//! Utilities for importing packages from and to `OneLauncher`.
|
||||
|
||||
pub mod content;
|
||||
pub mod from;
|
||||
pub mod import;
|
||||
pub mod infer;
|
||||
pub mod mrpack;
|
||||
pub mod zip;
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
//! Import and Export `.mrpack` mod packs.
|
||||
|
||||
use crate::package::from::{set_cluster_information, EnvType, PackFile, PackFileHash};
|
||||
use crate::prelude::ClusterPath;
|
||||
use crate::proxy::ingress_try_for_each;
|
||||
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
|
||||
use crate::store::{ClusterStage, Clusters, PackageSide};
|
||||
use crate::utils::http::{fetch_from_mirrors, write};
|
||||
use crate::{cluster, IngressType, State};
|
||||
use async_zip::base::read::seek::ZipFileReader;
|
||||
use onelauncher_utils::io;
|
||||
|
||||
use futures::StreamExt;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Component, PathBuf};
|
||||
|
||||
use super::from::{
|
||||
generate_pack_from_file, generate_pack_from_version_id, CreatePack, CreatePackLocation,
|
||||
PackFormat,
|
||||
};
|
||||
|
||||
/// Install a pack
|
||||
/// Wrapper around [`install_pack_files`] that generates a pack creation description, and
|
||||
/// attempts to install the pack files. If it fails, it will remove the cluster (fail safely)
|
||||
/// Install a modpack from a mrpack file (a modrinth .zip format)
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn install_zipped_mrpack(
|
||||
location: CreatePackLocation,
|
||||
cluster_path: ClusterPath,
|
||||
) -> crate::Result<ClusterPath> {
|
||||
let create_pack: CreatePack = match location {
|
||||
CreatePackLocation::FromModrinth {
|
||||
package_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
} => {
|
||||
generate_pack_from_version_id(
|
||||
package_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
cluster_path.clone(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
CreatePackLocation::FromFile { path } => {
|
||||
generate_pack_from_file(path, cluster_path.clone()).await?
|
||||
}
|
||||
};
|
||||
|
||||
let result = install_zipped_mrpack_files(create_pack, false).await;
|
||||
tokio::task::spawn(Clusters::update_versions());
|
||||
|
||||
match result {
|
||||
Ok(cluster) => Ok(cluster),
|
||||
Err(err) => {
|
||||
let _ = cluster::remove(&cluster_path).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install all pack files from a description. Does not remove the cluster if it fails
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn install_zipped_mrpack_files(
|
||||
create_pack: CreatePack,
|
||||
ignore_lock: bool,
|
||||
) -> crate::Result<ClusterPath> {
|
||||
let state = &State::get().await?;
|
||||
let file = create_pack.file;
|
||||
let description = create_pack.description.clone(); // make a copy for cluster::edit
|
||||
let icon = create_pack.description.icon;
|
||||
let package_id = create_pack.description.package_id;
|
||||
let version_id = create_pack.description.version_id;
|
||||
let existing_ingress = create_pack.description.existing_ingress;
|
||||
let cluster_path = create_pack.description.cluster_path;
|
||||
let icon_exists = icon.is_some();
|
||||
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
|
||||
|
||||
let mut zip_reader = ZipFileReader::with_tokio(reader).await?;
|
||||
|
||||
let zip_index_option = zip_reader
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.position(|f| f.filename().as_str().unwrap_or_default() == "modrinth.index.json");
|
||||
if let Some(zip_index) = zip_index_option {
|
||||
let mut manifest = String::new();
|
||||
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
|
||||
reader.read_to_string_checked(&mut manifest).await?;
|
||||
let pack: PackFormat = serde_json::from_str(&manifest)?;
|
||||
|
||||
if &*pack.game != "minecraft" {
|
||||
return Err(anyhow::anyhow!("pack doesn't support Minecraft").into());
|
||||
}
|
||||
|
||||
set_cluster_information(
|
||||
cluster_path.clone(),
|
||||
&description,
|
||||
&pack.name,
|
||||
&pack.dependencies,
|
||||
ignore_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cluster_path = cluster_path.clone();
|
||||
let ingress = init_or_edit_ingress(
|
||||
existing_ingress,
|
||||
IngressType::DownloadPackage {
|
||||
cluster_path: cluster_path.full_path().await?.clone(),
|
||||
package_name: pack.name.clone(),
|
||||
icon,
|
||||
package_id,
|
||||
package_version: version_id,
|
||||
},
|
||||
100.0,
|
||||
"downloading modpack",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let num_files = pack.files.len();
|
||||
ingress_try_for_each(
|
||||
futures::stream::iter(pack.files.into_iter()).map(Ok::<PackFile, crate::Error>),
|
||||
None,
|
||||
Some(&ingress),
|
||||
70.0,
|
||||
num_files,
|
||||
None,
|
||||
|pack| {
|
||||
let cluster_path = cluster_path.clone();
|
||||
async move {
|
||||
// TODO: prompt user for optional files in a modpack
|
||||
if let Some(env) = pack.env {
|
||||
if env
|
||||
.get(&EnvType::Client)
|
||||
.is_some_and(|x| x == &PackageSide::Unsupported)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let file = fetch_from_mirrors(
|
||||
&pack.downloads.iter().map(|x| &**x).collect::<Vec<&str>>(),
|
||||
pack.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let package_path = pack.path.to_string();
|
||||
let path = std::path::Path::new(&package_path).components().next();
|
||||
if let Some(Component::CurDir | Component::Normal(_)) = path {
|
||||
let path = cluster_path.full_path().await?.join(&package_path);
|
||||
write(&path, &file, &state.io_semaphore).await?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_ingress(&ingress, 0.0, Some("extracting overrides")).await?;
|
||||
|
||||
let mut total_len = 0;
|
||||
|
||||
for index in 0..zip_reader.file().entries().len() {
|
||||
let file = zip_reader.file().entries().get(index).unwrap();
|
||||
let filename = file.filename().as_str().unwrap_or_default();
|
||||
|
||||
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
|
||||
&& !filename.ends_with('/')
|
||||
{
|
||||
total_len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for index in 0..zip_reader.file().entries().len() {
|
||||
let file = zip_reader.file().entries().get(index).unwrap();
|
||||
|
||||
let filename = file.filename().as_str().unwrap_or_default();
|
||||
|
||||
let file_path = PathBuf::from(filename);
|
||||
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
|
||||
&& !filename.ends_with('/')
|
||||
{
|
||||
let mut content = Vec::new();
|
||||
let mut reader = zip_reader.reader_with_entry(index).await?;
|
||||
reader.read_to_end_checked(&mut content).await?;
|
||||
|
||||
let mut new_path = PathBuf::new();
|
||||
let components = file_path.components().skip(1);
|
||||
|
||||
for component in components {
|
||||
new_path.push(component);
|
||||
}
|
||||
|
||||
if new_path.file_name().is_some() {
|
||||
write(
|
||||
&cluster_path.full_path().await?.join(new_path),
|
||||
&content,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
send_ingress(
|
||||
&ingress,
|
||||
30.0 / f64::from(total_len),
|
||||
Some(&format!("extracting override {index}/{total_len}")),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let potential_icon = cluster_path.full_path().await?.join("icon.png");
|
||||
if !icon_exists && potential_icon.exists() {
|
||||
cluster::edit_icon(&cluster_path, Some(&potential_icon)).await?;
|
||||
}
|
||||
|
||||
if let Some(cluster) = cluster::get(&cluster_path).await? {
|
||||
crate::game::install_minecraft(&cluster, Some(ingress), false).await?;
|
||||
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok::<ClusterPath, crate::Error>(cluster_path.clone())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("no pack manifest found in mrpack").into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(mrpack_file))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn remove_all_related_files(
|
||||
cluster_path: ClusterPath,
|
||||
mrpack_file: bytes::Bytes,
|
||||
) -> crate::Result<()> {
|
||||
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
|
||||
|
||||
let mut zip_reader = ZipFileReader::with_tokio(reader).await?;
|
||||
|
||||
let zip_index_option = zip_reader
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.position(|f| f.filename().as_str().unwrap_or_default() == "modrinth.index.json");
|
||||
if let Some(zip_index) = zip_index_option {
|
||||
let mut manifest = String::new();
|
||||
|
||||
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
|
||||
reader.read_to_string_checked(&mut manifest).await?;
|
||||
|
||||
let pack: PackFormat = serde_json::from_str(&manifest)?;
|
||||
|
||||
if &*pack.game != "minecraft" {
|
||||
return Err(anyhow::anyhow!("pack doesn't support Minecraft").into());
|
||||
}
|
||||
|
||||
cluster::edit(&cluster_path, |cl| {
|
||||
cl.stage = ClusterStage::PackDownloading;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// let state = State::get().await?;
|
||||
// TODO: Packages
|
||||
// let all_hashes = pack
|
||||
// .files
|
||||
// .iter()
|
||||
// .filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
// let files_url = format!("{}version_files", crate::constants::MODRINTH_API_URL);
|
||||
|
||||
// let hash_packages = fetch_json::<HashMap<String, ManagedVersion>>(
|
||||
// Method::POST,
|
||||
// &files_url,
|
||||
// None,
|
||||
// Some(json!({
|
||||
// "hashes": all_hashes,
|
||||
// "algorithm": "sha512",
|
||||
// })),
|
||||
// &state.fetch_semaphore,
|
||||
// )
|
||||
// .await?;
|
||||
// let to_remove = hash_packages
|
||||
// .into_values()
|
||||
// .map(|p| p.package_id)
|
||||
// .collect::<Vec<_>>();
|
||||
// let cluster = cluster::get(&cluster_path).await?.ok_or_else(|| {
|
||||
// anyhow::anyhow!("{} is an unmanaged cluster!", cluster_path.to_string())
|
||||
// })?;
|
||||
|
||||
// for (package_path, package) in &cluster.packages {
|
||||
// if let PackageMetadata::Managed { package_id, .. } = &package.meta {
|
||||
// if to_remove.contains(package_id) {
|
||||
// let path = cluster.get_full_path().await?.join(package_path.0.clone());
|
||||
// if path.exists() {
|
||||
// io::remove_file(&path).await?;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
for file in pack.files {
|
||||
let path: PathBuf = cluster_path.full_path().await?.join(file.path.to_string());
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
for index in 0..zip_reader.file().entries().len() {
|
||||
let file = zip_reader.file().entries().get(index).unwrap();
|
||||
let filename = file.filename().as_str().unwrap_or_default();
|
||||
let file_path = PathBuf::from(filename);
|
||||
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
|
||||
&& !filename.ends_with('/')
|
||||
{
|
||||
let mut new_path = PathBuf::new();
|
||||
let components = file_path.components().skip(1);
|
||||
|
||||
for component in components {
|
||||
new_path.push(component);
|
||||
}
|
||||
|
||||
let existing_file = cluster_path.full_path().await?.join(&new_path);
|
||||
if existing_file.exists() {
|
||||
io::remove_file(&existing_file).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("no pack manifest found in mrpack").into())
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
//! Import and Export `.zip` mod packs
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
//! Process management interface
|
||||
|
||||
pub use crate::store::{Cluster, InitHooks, JavaOptions, Memory, Resolution, Settings, State};
|
||||
use crate::store::{ClusterPath, ProcessorChild};
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct DetailedProcess {
|
||||
pub uuid: Uuid,
|
||||
pub user: Option<Uuid>,
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
impl DetailedProcess {
|
||||
pub async fn from_processor_child(process: &ProcessorChild) -> Self {
|
||||
let pid = process.current_child.read().await.id().unwrap_or(0);
|
||||
Self {
|
||||
uuid: process.uuid,
|
||||
user: process.user,
|
||||
started_at: process.started_at,
|
||||
pid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// get detailed processes by a [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_process_detailed_by_id(uuid: Uuid) -> crate::Result<DetailedProcess> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
|
||||
let child = processor
|
||||
.get(uuid)
|
||||
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
|
||||
let child = child.read().await;
|
||||
let pid = child
|
||||
.current_child
|
||||
.read()
|
||||
.await
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
|
||||
Ok(DetailedProcess {
|
||||
uuid,
|
||||
user: child.user,
|
||||
started_at: child.started_at,
|
||||
pid,
|
||||
})
|
||||
}
|
||||
|
||||
/// get detailed processes by a [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_processes_detailed_by_path(
|
||||
path: ClusterPath,
|
||||
) -> crate::Result<Vec<DetailedProcess>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
|
||||
let uuids = processor.running_cluster(path).await?;
|
||||
let mut processes = Vec::new();
|
||||
for uuid in uuids {
|
||||
let child = processor
|
||||
.get(uuid)
|
||||
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
|
||||
let child = child.read().await;
|
||||
let pid = child
|
||||
.current_child
|
||||
.read()
|
||||
.await
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
|
||||
processes.push(DetailedProcess {
|
||||
uuid,
|
||||
user: child.user,
|
||||
started_at: child.started_at,
|
||||
pid,
|
||||
});
|
||||
}
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
/// check whether or not a process has completed by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn uuid_is_finished(uuid: Uuid) -> crate::Result<bool> {
|
||||
Ok(uuid_exit_status(uuid).await?.is_some())
|
||||
}
|
||||
|
||||
/// get the user from a processor by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user_by_process(uuid: Uuid) -> crate::Result<Option<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
if let Some(processor) = processor.get(uuid) {
|
||||
Ok(processor.read().await.user)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("process not found").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// get the user from a processor by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_process_started_at(uuid: Uuid) -> crate::Result<DateTime<Utc>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
if let Some(processor) = processor.get(uuid) {
|
||||
Ok(processor.read().await.started_at)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("process not found").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// check the exit status of a process by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn uuid_exit_status(uuid: Uuid) -> crate::Result<Option<i32>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
processor.exit_status(uuid).await
|
||||
}
|
||||
|
||||
/// get all process [`Uuid`]s.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_uuids() -> crate::Result<Vec<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
Ok(processor.keys())
|
||||
}
|
||||
|
||||
/// get all running process [`Uuid`]s.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_running() -> crate::Result<Vec<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
processor.running().await
|
||||
}
|
||||
|
||||
/// get all running process' [`ClusterPath`]s.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_running_cluster_paths() -> crate::Result<Vec<ClusterPath>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
processor.running_cluster_paths().await
|
||||
}
|
||||
|
||||
/// get all running process' [`Cluster`]s.
|
||||
#[tracing::instrument]
|
||||
pub async fn get_running_clusters() -> crate::Result<Vec<Cluster>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
processor.running_clusters().await
|
||||
}
|
||||
|
||||
/// check if a cluster is running by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn is_cluster_running(uuid: Uuid) -> crate::Result<bool> {
|
||||
let clusters = get_running_clusters().await?;
|
||||
Ok(clusters.iter().any(|cluster| cluster.uuid == uuid))
|
||||
}
|
||||
|
||||
/// get all processes by a [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_uuids_by_cluster_path(cluster_path: ClusterPath) -> crate::Result<Vec<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
processor.running_cluster(cluster_path).await
|
||||
}
|
||||
|
||||
/// kill an existing and running process by its [`Uuid`]
|
||||
#[tracing::instrument]
|
||||
pub async fn kill_by_uuid(uuid: Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
|
||||
if let Some(process) = processor.get(uuid) {
|
||||
let mut process = process.write().await;
|
||||
kill(&mut process).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// wait for a child process to finish running its manager by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn wait_for_by_uuid(uuid: Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
|
||||
if let Some(process) = processor.get(uuid) {
|
||||
let mut process = process.write().await;
|
||||
wait_for(&mut process).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// kill an existing and running process
|
||||
#[tracing::instrument(skip(process))]
|
||||
pub async fn kill(process: &mut ProcessorChild) -> crate::Result<()> {
|
||||
process.current_child.write().await.kill().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// wait for a child process to finish running its manager
|
||||
#[tracing::instrument(skip(process))]
|
||||
pub async fn wait_for(process: &mut ProcessorChild) -> crate::Result<()> {
|
||||
process
|
||||
.manager
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("manager already completed for process {}", process.uuid))?
|
||||
.await?
|
||||
.map_err(|err| anyhow::anyhow!("failed to run minecraft: {err}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get process pid by its [`Uuid`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_pid_by_uuid(uuid: Uuid) -> crate::Result<u32> {
|
||||
let state = State::get().await?;
|
||||
let processor = state.processor.read().await;
|
||||
Ok(processor
|
||||
.get(uuid)
|
||||
.ok_or_else(|| anyhow::anyhow!("process not found"))?
|
||||
.read()
|
||||
.await
|
||||
.current_child
|
||||
.read()
|
||||
.await
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process not found"))?)
|
||||
}
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
//! Proxy for Tauri/CLI specific features and communication
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{OnceCell, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::store::{ClusterPath, IngressProcessor};
|
||||
|
||||
pub mod send;
|
||||
pub mod utils;
|
||||
pub use utils::*;
|
||||
|
||||
static PROXY_STATE: OnceCell<Arc<ProxyState>> = OnceCell::const_new();
|
||||
pub struct ProxyState {
|
||||
#[cfg(feature = "tauri")]
|
||||
pub app: tauri::AppHandle<tauri::Wry>,
|
||||
pub ingress_feeds: RwLock<HashMap<Uuid, Ingress>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct Ingress {
|
||||
pub ingress_uuid: Uuid,
|
||||
pub message: String,
|
||||
pub ingress_type: IngressType,
|
||||
pub total: f64,
|
||||
pub current: f64,
|
||||
#[serde(skip)]
|
||||
pub last_sent: f64,
|
||||
#[cfg(feature = "cli")]
|
||||
#[serde(skip)]
|
||||
pub cli: indicatif::ProgressBar,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct IngressId(Uuid);
|
||||
|
||||
impl Drop for IngressId {
|
||||
#[clippy::has_significant_drop]
|
||||
fn drop(&mut self) {
|
||||
let ingress_uuid = self.0;
|
||||
tokio::spawn(async move {
|
||||
if let Ok(proxy_state) = ProxyState::get().await {
|
||||
let mut ingress_feeds = proxy_state.ingress_feeds.write().await;
|
||||
#[cfg(any(feature = "tauri", feature = "cli"))]
|
||||
if let Some(ingress) = ingress_feeds.remove(&ingress_uuid) {
|
||||
#[cfg(feature = "cli")]
|
||||
{
|
||||
let cli = ingress.cli;
|
||||
cli.finish();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
use tauri::Emitter;
|
||||
let event = ingress.ingress_type.clone();
|
||||
let fraction = ingress.current / ingress.total;
|
||||
|
||||
let _ = proxy_state.app.emit(
|
||||
"ingress",
|
||||
IngressPayload {
|
||||
fraction: None,
|
||||
message: "Complete".to_string(),
|
||||
event,
|
||||
ingress_uuid,
|
||||
},
|
||||
);
|
||||
|
||||
tracing::trace!("exited at {fraction} for ingress: {:?}", ingress_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "tauri", feature = "cli")))]
|
||||
ingress_feeds.remove(&ingress_uuid);
|
||||
}
|
||||
|
||||
if crate::State::initalized() {
|
||||
let _ = IngressProcessor::finish(
|
||||
crate::store::IngressProcessType::IngressFeed,
|
||||
ingress_uuid,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyState {
|
||||
#[cfg(feature = "tauri")]
|
||||
pub async fn initialize(handle: &tauri::AppHandle<tauri::Wry>) -> crate::Result<Arc<Self>> {
|
||||
PROXY_STATE
|
||||
.get_or_try_init(|| async {
|
||||
Ok(Arc::new(Self {
|
||||
app: handle.clone(),
|
||||
ingress_feeds: RwLock::new(HashMap::new()),
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tauri"))]
|
||||
pub async fn initialize() -> crate::Result<Arc<Self>> {
|
||||
PROXY_STATE
|
||||
.get_or_try_init(|| async {
|
||||
Ok(Arc::new(Self {
|
||||
ingress_feeds: RwLock::new(HashMap::new()),
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
pub async fn get_main_window() -> crate::Result<tauri::WebviewWindow> {
|
||||
use tauri::Manager;
|
||||
let value = Self::get().await?;
|
||||
Ok(value.app.get_webview_window("main").unwrap())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
#[cfg(feature = "tauri")]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tauri"))]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
Self::initialize().await
|
||||
}
|
||||
|
||||
pub async fn list_ingress_feeds() -> crate::Result<HashMap<Uuid, Ingress>> {
|
||||
let value = Self::get().await?;
|
||||
let read = value.ingress_feeds.read().await;
|
||||
|
||||
let mut display_list: HashMap<Uuid, Ingress> = HashMap::new();
|
||||
for (uuid, ingress) in read.iter() {
|
||||
display_list.insert(*uuid, ingress.clone());
|
||||
}
|
||||
|
||||
Ok(display_list)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IngressType {
|
||||
Initialize,
|
||||
DownloadJava {
|
||||
version: u32,
|
||||
},
|
||||
DownloadLoader {
|
||||
cluster_path: PathBuf,
|
||||
cluster_name: String,
|
||||
},
|
||||
SyncCluster {
|
||||
cluster_path: PathBuf,
|
||||
cluster_name: String,
|
||||
},
|
||||
InferPackages,
|
||||
CopyCluster {
|
||||
import: PathBuf,
|
||||
cluster_name: String,
|
||||
},
|
||||
SyncConfig {
|
||||
new_path: PathBuf,
|
||||
},
|
||||
Archival {
|
||||
cluster_path: PathBuf,
|
||||
cluster_name: String,
|
||||
},
|
||||
DownloadPackage {
|
||||
cluster_path: PathBuf,
|
||||
package_name: String,
|
||||
icon: Option<PathBuf>,
|
||||
package_id: Option<String>,
|
||||
package_version: Option<String>,
|
||||
},
|
||||
DownloadPack {
|
||||
cluster_path: PathBuf,
|
||||
package_name: String,
|
||||
icon: Option<String>,
|
||||
package_version: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct IngressPayload {
|
||||
pub event: IngressType,
|
||||
pub ingress_uuid: Uuid,
|
||||
pub fraction: Option<f64>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct OfflinePayload {
|
||||
pub offline: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct MessagePayload {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(tag = "event")]
|
||||
pub enum InternetPayload {
|
||||
InstallPackage { id: String },
|
||||
InstallPack { id: String },
|
||||
InstallPath { path: PathBuf },
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ProcessPayload {
|
||||
pub uuid: Uuid,
|
||||
pub pid: u32,
|
||||
pub event: ProcessPayloadType,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProcessPayloadType {
|
||||
Started,
|
||||
Modified,
|
||||
Finished,
|
||||
Logging,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ClusterPayload {
|
||||
pub uuid: Uuid,
|
||||
pub cluster_path: ClusterPath,
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub event: ClusterPayloadType,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ClusterPayloadType {
|
||||
Created,
|
||||
Inserted,
|
||||
Synced,
|
||||
Edited,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ProxyError {
|
||||
#[error("event state was not initialized!")]
|
||||
NotInitialized,
|
||||
|
||||
#[error("non-existant ingress of key: {0}")]
|
||||
NoIngressFound(Uuid),
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
#[error("Tauri error: {0}")]
|
||||
TauriError(#[from] tauri::Error),
|
||||
}
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
use super::IngressId;
|
||||
use crate::api::proxy::{
|
||||
ClusterPayloadType, Ingress, IngressType, InternetPayload, ProcessPayloadType, ProxyError,
|
||||
};
|
||||
use crate::proxy::OfflinePayload;
|
||||
use crate::store::{ClusterPath, IngressProcessType, IngressProcessor};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use crate::constants::CLI_TOTAL_INGRESS;
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
use tauri_specta::Event;
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
use crate::api::proxy::{ClusterPayload, IngressPayload, MessagePayload, ProcessPayload};
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
use tauri::Emitter;
|
||||
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn init_ingress(
|
||||
ingress_type: IngressType,
|
||||
total: f64,
|
||||
title: &str,
|
||||
) -> crate::Result<IngressId> {
|
||||
let key = init_ingress_internal(ingress_type, total, title).await?;
|
||||
IngressProcessor::add_ingress(IngressProcessType::IngressFeed, key.0).await?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn init_ingress_internal(
|
||||
ingress_type: IngressType,
|
||||
total: f64,
|
||||
title: &str,
|
||||
) -> crate::Result<IngressId> {
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
let key = IngressId(Uuid::new_v4());
|
||||
|
||||
proxy_state.ingress_feeds.write().await.insert(
|
||||
key.0,
|
||||
Ingress {
|
||||
ingress_uuid: key.0,
|
||||
message: title.to_string(),
|
||||
total,
|
||||
current: 0.0,
|
||||
last_sent: 0.0,
|
||||
ingress_type,
|
||||
#[cfg(feature = "cli")]
|
||||
cli: {
|
||||
let pb = indicatif::ProgressBar::new(CLI_TOTAL_INGRESS);
|
||||
pb.set_position(0);
|
||||
pb.set_style(indicatif::ProgressStyle::default_bar().template(
|
||||
"{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}"
|
||||
).unwrap().progress_chars("#>-"));
|
||||
pb
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
send_ingress(&key, 0.0, None).await?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub async fn init_or_edit_ingress(
|
||||
id: Option<IngressId>,
|
||||
ingress_type: IngressType,
|
||||
total: f64,
|
||||
title: &str,
|
||||
) -> crate::Result<IngressId> {
|
||||
if let Some(id) = id {
|
||||
edit_ingress(&id, ingress_type, total, title).await?;
|
||||
|
||||
Ok(id)
|
||||
} else {
|
||||
init_ingress(ingress_type, total, title).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn edit_ingress(
|
||||
id: &IngressId,
|
||||
ingress_type: IngressType,
|
||||
total: f64,
|
||||
title: &str,
|
||||
) -> crate::Result<()> {
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
let ingress_feeds = proxy_state.ingress_feeds.write();
|
||||
|
||||
if let Some(ingress) = ingress_feeds.await.get_mut(&id.0) {
|
||||
ingress.ingress_type = ingress_type;
|
||||
ingress.total = total;
|
||||
ingress.message = title.to_string();
|
||||
ingress.current = 0.0;
|
||||
ingress.last_sent = 0.0;
|
||||
#[cfg(feature = "cli")]
|
||||
{
|
||||
ingress.cli.reset();
|
||||
}
|
||||
};
|
||||
|
||||
send_ingress(id, 0.0, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[tracing::instrument(level = "debug")]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn send_ingress(
|
||||
key: &IngressId,
|
||||
increment: f64,
|
||||
message: Option<&str>,
|
||||
) -> crate::Result<()> {
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
let mut ingress = proxy_state.ingress_feeds.write().await;
|
||||
|
||||
let Some(ingress) = ingress.get_mut(&key.0) else {
|
||||
return Err(ProxyError::NoIngressFound(key.0).into());
|
||||
};
|
||||
|
||||
ingress.current += increment;
|
||||
let display = ingress.current / ingress.total;
|
||||
let display_conv = if display >= 1.0 { None } else { Some(display) };
|
||||
|
||||
if f64::abs(display - ingress.last_sent) > 0.005 {
|
||||
#[cfg(feature = "cli")]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
{
|
||||
ingress
|
||||
.cli
|
||||
.set_message(message.map_or_else(|| ingress.message.clone(), ToString::to_string));
|
||||
ingress
|
||||
.cli
|
||||
.set_position((display * CLI_TOTAL_INGRESS as f64).round() as u64);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
IngressPayload::NAME,
|
||||
IngressPayload {
|
||||
fraction: display_conv,
|
||||
message: message.unwrap_or(&ingress.message).to_string(),
|
||||
event: ingress.ingress_type.clone(),
|
||||
ingress_uuid: ingress.ingress_uuid,
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
|
||||
ingress.last_sent = display;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_message(message: &str) -> crate::Result<()> {
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
MessagePayload::NAME,
|
||||
MessagePayload {
|
||||
message: message.to_string(),
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
tracing::warn!("{}", message);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_offline(offline: bool) -> crate::Result<()> {
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(OfflinePayload::NAME, OfflinePayload { offline })
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_internet(internet: InternetPayload) -> crate::Result<()> {
|
||||
tracing::debug!("operation {}", serde_json::to_string(&internet)?);
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(InternetPayload::NAME, internet)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_process(
|
||||
uuid: Uuid,
|
||||
pid: u32,
|
||||
event: ProcessPayloadType,
|
||||
message: &str,
|
||||
) -> crate::Result<()> {
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
ProcessPayload::NAME,
|
||||
ProcessPayload {
|
||||
uuid,
|
||||
pid,
|
||||
event,
|
||||
message: message.to_string(),
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_cluster(
|
||||
uuid: Uuid,
|
||||
cluster_path: &ClusterPath,
|
||||
name: &str,
|
||||
event: ClusterPayloadType,
|
||||
) -> crate::Result<()> {
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let path = cluster_path.full_path().await?;
|
||||
let proxy_state = crate::ProxyState::get().await?;
|
||||
proxy_state
|
||||
.app
|
||||
.emit(
|
||||
ClusterPayload::NAME,
|
||||
ClusterPayload {
|
||||
uuid,
|
||||
cluster_path: cluster_path.clone(),
|
||||
path,
|
||||
name: name.to_string(),
|
||||
event,
|
||||
},
|
||||
)
|
||||
.map_err(ProxyError::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
use futures::prelude::*;
|
||||
|
||||
use crate::proxy::send::send_ingress;
|
||||
use crate::proxy::IngressId;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! count {
|
||||
() => (0usize);
|
||||
( $x:tt $($xs:tt)* ) => (1usize + $crate::count!($($xs)*));
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ingress_join {
|
||||
($key:expr, $total:expr, $msg:expr; $($task:expr $(,)?)+) => {
|
||||
{
|
||||
let key = $key;
|
||||
let msg: Option<&str> = $msg;
|
||||
let num_futures = $crate::count!($($task)*);
|
||||
let i = $total / num_futures as f64;
|
||||
|
||||
paste::paste! {
|
||||
$( let [ <unique_name $task> ] = {
|
||||
{
|
||||
let key = key.clone();
|
||||
let msg = msg.clone();
|
||||
async move {
|
||||
let res = $task.await;
|
||||
if let Some(key) = key {
|
||||
$crate::api::proxy::send::send_ingress(key, i, msg).await?;
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
};)+
|
||||
}
|
||||
|
||||
paste::paste! {
|
||||
tokio::try_join! (
|
||||
$( [ <unique_name $task>] ),+
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// TryStreamExt concurrently for Ingress feeds
|
||||
#[tracing::instrument(skip(stream, f))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn ingress_try_for_each<I, F, Fut, T>(
|
||||
stream: I,
|
||||
limit: Option<usize>,
|
||||
key: Option<&IngressId>,
|
||||
total: f64,
|
||||
num_futs: usize,
|
||||
message: Option<&str>,
|
||||
f: F,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
I: futures::TryStreamExt<Error = crate::Error> + TryStream<Ok = T>,
|
||||
F: FnMut(T) -> Fut + Send,
|
||||
Fut: Future<Output = crate::Result<()>> + Send,
|
||||
T: Send,
|
||||
{
|
||||
let mut f = f;
|
||||
stream
|
||||
.try_for_each_concurrent(limit, |item| {
|
||||
let f = f(item);
|
||||
async move {
|
||||
f.await?;
|
||||
if let Some(key) = key {
|
||||
send_ingress(key, total / (num_futs as f64), message).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
//! Settings Management
|
||||
|
||||
use crate::proxy::send::{init_ingress, send_ingress};
|
||||
use crate::store::{Clusters, Directories, Settings};
|
||||
use crate::State;
|
||||
use onelauncher_utils::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// gets a [`Settings`] object state.
|
||||
#[tracing::instrument]
|
||||
pub async fn get() -> crate::Result<Settings> {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
Ok(settings.clone())
|
||||
}
|
||||
|
||||
/// sets a [`Settings`] object state.
|
||||
#[tracing::instrument]
|
||||
pub async fn set(settings: Settings) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
if settings.config_dir != state.settings.read().await.config_dir {
|
||||
return Err(anyhow::anyhow!("cannot change config directory as a setting").into());
|
||||
}
|
||||
|
||||
let (io, fetch) = async {
|
||||
let read = state.settings.read().await;
|
||||
(
|
||||
settings.max_async_io_operations != read.max_async_io_operations,
|
||||
settings.max_async_fetches != read.max_async_fetches,
|
||||
)
|
||||
}
|
||||
.await;
|
||||
|
||||
let discord_rpc = {
|
||||
let read = state.settings.read().await;
|
||||
settings.disable_discord != read.disable_discord
|
||||
};
|
||||
|
||||
{
|
||||
*state.settings.write().await = settings;
|
||||
}
|
||||
|
||||
if discord_rpc {
|
||||
state.discord_rpc.clear(true).await?;
|
||||
}
|
||||
|
||||
if io {
|
||||
state.reset_io_semaphore().await;
|
||||
}
|
||||
|
||||
if fetch {
|
||||
state.reset_fetch_semaphore().await;
|
||||
}
|
||||
|
||||
State::sync().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// sets the config and caches directory, this can have side effects.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn set_directory(new: PathBuf) -> crate::Result<()> {
|
||||
tracing::trace!("changing config directory to {}", new.display());
|
||||
|
||||
if !new.is_dir() {
|
||||
return Err(
|
||||
anyhow::anyhow!("new config directory {} is not a folder!", new.display()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
if !is_writable(new.clone()).await? {
|
||||
return Err(
|
||||
anyhow::anyhow!("new config directory {} is not writeable", new.display()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
let ingress = init_ingress(
|
||||
crate::IngressType::SyncConfig {
|
||||
new_path: new.clone(),
|
||||
},
|
||||
100.0,
|
||||
"syncing and changing configuration directory",
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::trace!("changing config directory, overriding state write access");
|
||||
let mut state_write = State::get_and_write().await?;
|
||||
let old = state_write.directories.config_dir.read().await.clone();
|
||||
|
||||
tracing::trace!("resetting file watcher after changing config directory");
|
||||
let fs_watcher = crate::utils::watcher::initialize_watcher()?;
|
||||
state_write.watcher = RwLock::new(fs_watcher);
|
||||
|
||||
tracing::trace!("collecting all config files to be transfered");
|
||||
let mut cfg_entries = io::read_dir(&old).await?;
|
||||
let across_filesystems = is_different_fs(&old, &new);
|
||||
let mut entries = vec![];
|
||||
let mut cleanable = vec![];
|
||||
|
||||
while let Some(e) = cfg_entries
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|err| io::IOError::with_path(err, &old))?
|
||||
{
|
||||
let epath = e.path();
|
||||
if let Some(file_name) = epath.file_name() {
|
||||
if file_name == crate::constants::CLUSTERS_FOLDER
|
||||
|| file_name == crate::constants::METADATA_FOLDER
|
||||
{
|
||||
if across_filesystems {
|
||||
entries.extend(crate::package::import::sub(&epath, false).await?);
|
||||
cleanable.push(epath.clone());
|
||||
} else {
|
||||
entries.push(epath.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("transferring configuration files");
|
||||
let io_semaphore = &state_write.io_semaphore;
|
||||
let nentries = entries.len() as f64;
|
||||
for epath in entries {
|
||||
let relpath = epath
|
||||
.strip_prefix(&old)
|
||||
.map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?;
|
||||
let newpath = new.join(relpath);
|
||||
if across_filesystems {
|
||||
crate::utils::http::copy(&epath, &newpath, io_semaphore).await?;
|
||||
} else {
|
||||
io::rename(epath.clone(), newpath.clone()).await?;
|
||||
}
|
||||
|
||||
send_ingress(&ingress, 80.0 * (1.0 / nentries), None).await?;
|
||||
}
|
||||
|
||||
tracing::trace!("re-setting configuration settings");
|
||||
let settings = {
|
||||
let mut settings = state_write.settings.write().await;
|
||||
settings.config_dir = Some(new.clone());
|
||||
|
||||
tracing::trace!("updating java paths and keys");
|
||||
for j in settings.java_versions.keys() {
|
||||
if let Some(java) = settings.java_versions.get_mut(&j) {
|
||||
if let Ok(relpath) = PathBuf::from(java.path.clone()).strip_prefix(&old) {
|
||||
java.path = new.join(relpath).to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("re-syncing settings file");
|
||||
settings
|
||||
.sync(&state_write.directories.settings_file())
|
||||
.await?;
|
||||
settings.clone()
|
||||
};
|
||||
|
||||
tracing::trace!("re-initializing the config directory");
|
||||
state_write.directories = Directories::initalize(&settings)?;
|
||||
let cleanable_nentries = cleanable.len();
|
||||
if cleanable_nentries > 0 {
|
||||
tracing::trace!("deleting old config files");
|
||||
}
|
||||
for ce in cleanable {
|
||||
io::remove_dir_all(ce).await?;
|
||||
send_ingress(&ingress, 10.0 * (1.0 / cleanable_nentries as f64), None).await?;
|
||||
}
|
||||
|
||||
tracing::trace!("re-resetting file watching system");
|
||||
let mut file_watcher = crate::utils::watcher::initialize_watcher()?;
|
||||
state_write.clusters =
|
||||
RwLock::new(Clusters::initialize(&state_write.directories, &mut file_watcher).await?);
|
||||
state_write.watcher = RwLock::new(file_watcher);
|
||||
send_ingress(&ingress, 10.0, None).await?;
|
||||
|
||||
tracing::info!(
|
||||
"successfully switched configuration directory to {}",
|
||||
new.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// checks if two paths are on different data storage devices
|
||||
fn is_different_fs(first: &Path, second: &Path) -> bool {
|
||||
let first_root = first.components().next();
|
||||
let second_root = second.components().next();
|
||||
first_root != second_root
|
||||
}
|
||||
|
||||
/// checks if a [`PathBuf`] is writable or locked.
|
||||
pub async fn is_writable(path: PathBuf) -> crate::Result<bool> {
|
||||
let tmp = path.join(".tmp");
|
||||
match io::write(tmp.clone(), "test").await {
|
||||
Ok(()) => {
|
||||
io::remove_file(tmp).await?;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("failed to write to config directory: {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
//! **`OneLauncher` Constants**
|
||||
//!
|
||||
//! Public constant variables and strings that are used throughout the launcher.
|
||||
//! Can be shared publically, any secrets should be stored in [`crate::store::credentials`].
|
||||
|
||||
// =========== Core Metadata ===========
|
||||
/// The name of the launcher.
|
||||
pub const NAME: &str = "OneLauncher";
|
||||
/// The version of the launcher (from `../Cargo.toml` env).
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
// =========== Default Ingress ===========
|
||||
/// The amount of attempts to fetch a file before a request fails.
|
||||
pub const FETCH_ATTEMPTS: usize = 3;
|
||||
/// The total ingress capacity of the CLI, provided to [`indicatif`].
|
||||
#[cfg(feature = "cli")]
|
||||
pub const CLI_TOTAL_INGRESS: u64 = 1000;
|
||||
/// The newline which we replace in metadata files.
|
||||
pub const DUMMY_REPLACE_NEWLINE: &str = "\n";
|
||||
|
||||
// =========== Authentication ===========
|
||||
/// Our Discord RPC client ID.
|
||||
pub const DISCORD_RPC_CLIENT_ID: &str = "1274084193193955408";
|
||||
/// Our Microsoft client ID.
|
||||
pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03";
|
||||
/// Mojang/Microsoft client ID.
|
||||
pub const MINECRAFT_CLIENT_ID: &str = "00000000402b5328";
|
||||
/// Mojang/Microsoft login redirect URI.
|
||||
pub const MINECRAFT_REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||
/// Mojang/Microsoft login xboxlive scopes to get tokens.
|
||||
pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
|
||||
// =========== API ===========
|
||||
// !!! URLS must NOT have a trailing slash. !!!
|
||||
/// The Modrinth API base url.
|
||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2";
|
||||
/// The Modrinth V3 API base url. Used for things like fetching information about organizations.
|
||||
pub const MODRINTH_V3_API_URL: &str = "https://api.modrinth.com/v3";
|
||||
/// The Curseforge API base url.
|
||||
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com";
|
||||
/// The Curseforge API key. Reserved for use in OneLauncher projects only. Do not use in other projects without permission from the OneLauncher team.
|
||||
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
|
||||
/// The Minecraft game ID on Curseforge.
|
||||
pub const CURSEFORGE_GAME_ID: u32 = 432;
|
||||
/// Our metadata API base url.
|
||||
pub const METADATA_API_URL: &str = "https://meta.polyfrost.org";
|
||||
/// Featured packages API url.
|
||||
pub const FEATURED_PACKAGES_URL: &str = "https://polyfrost.org/meta/onelauncher/featured.json";
|
||||
/// <https://mclo.gs>/ API base url.
|
||||
pub const MCLOGS_API_URL: &str = "https://api.mclo.gs/1";
|
||||
/// https://skyclient.co/ metadata base url.
|
||||
pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1";
|
||||
|
||||
// =========== Paths ===========
|
||||
/// The public `settings.json` file used to store the global [`Settings`] state.
|
||||
///
|
||||
/// [`Settings`]: crate::store::Settings
|
||||
pub const SETTINGS_FILE: &str = "settings.json";
|
||||
/// The public `cluster.json` file used to store the global [`Clusters`] state.
|
||||
///
|
||||
/// [`Clusters`]: crate::store::Clusters
|
||||
pub const CLUSTER_FILE: &str = "cluster.json";
|
||||
/// The public `authentication.json` file used to store the global [`MinecraftAuth`] state.
|
||||
///
|
||||
/// [`MinecraftAuth`]: crate::store::MinecraftAuth
|
||||
pub const AUTH_FILE: &str = "authentication.json";
|
||||
/// The public `processor.json` file used to store the global [`Processor`] state.
|
||||
///
|
||||
/// [`Processor`]: crate::store::Processor
|
||||
pub const PROCESSOR_FILE: &str = "processor.json";
|
||||
|
||||
/// The current [`Settings`] format version, bumped for breaking changes.
|
||||
/// If updated, a config file migration logic **NEEDS** to be implemented.
|
||||
///
|
||||
/// [`Settings`]: crate::store::Settings
|
||||
pub const CURRENT_SETTINGS_FORMAT_VERSION: u32 = 1;
|
||||
|
||||
/// The constant core caches folder.
|
||||
pub const CACHES_FOLDER: &str = "caches";
|
||||
/// The constant core crash reports folder.
|
||||
pub const CRASH_PATH: &str = "crash-reports";
|
||||
/// The constant core clusters folder.
|
||||
pub const CLUSTERS_FOLDER: &str = "clusters";
|
||||
/// The constant core metadata folder.
|
||||
pub const METADATA_FOLDER: &str = "metadata";
|
||||
|
||||
// =========== Hacky Mojang-spec OS constants ===========
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const TARGET_OS: &str = "windows";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const TARGET_OS: &str = "osx";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const TARGET_OS: &str = "linux";
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
pub const NATIVE_ARCH: &str = "32";
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub const NATIVE_ARCH: &str = "64";
|
||||
|
||||
#[cfg(all(not(target_arch = "x86_64"), not(target_arch = "x86")))]
|
||||
pub const NATIVE_ARCH: &str = "64";
|
||||
|
||||
// =========== Platform ===========
|
||||
/// The default java executable on Windows or anything else.
|
||||
pub const JAVA_BIN: &str = if cfg!(windows) { "javaw.exe" } else { "java" };
|
||||
|
||||
/// The bit width on x64 or x32 architectures.
|
||||
pub const ARCH_WIDTH: &str = if cfg!(target_pointer_width = "64") {
|
||||
"64"
|
||||
} else {
|
||||
"32"
|
||||
};
|
||||
|
||||
/// Evaluates to `\r\n` on windows, and `\n` on everything else.
|
||||
pub const LINE_ENDING: &str = if cfg!(windows) { "\r\n" } else { "\n" };
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
//! **`OneLauncher` Errors**
|
||||
//!
|
||||
//! Public wrappers around common `OneLauncher` errors.
|
||||
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
/// A standardized [`thiserror::Error`] type that is used across the launcher
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ErrorKind {
|
||||
/// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors.
|
||||
#[error("failed to acquire a tokio semaphore: {0}")]
|
||||
AcquireError(#[from] tokio::sync::AcquireError),
|
||||
|
||||
/// Wrapper around [`tokio::sync::oneshot::error::RecvError`] to handle recieve errors.
|
||||
#[error("failed to handle async reciever: {0}")]
|
||||
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
|
||||
|
||||
/// Wrapper around [`tokio::task::JoinError`] to handle tokio join handle errors.
|
||||
#[error("failed to handle an async task: {0}")]
|
||||
JoinError(#[from] tokio::task::JoinError),
|
||||
|
||||
/// Wrapper around [`flate2::CompressError`] to handle flate compression errors.
|
||||
#[error("failed to compress a file with flate: {0}")]
|
||||
FlateError(#[from] flate2::CompressError),
|
||||
|
||||
/// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors.
|
||||
#[error("failed to decompress a file with flate: {0}")]
|
||||
DeflateError(#[from] flate2::DecompressError),
|
||||
|
||||
/// Wrapper around [`uuid::Error`] to handle UUID parsing errors.
|
||||
#[error("failed to parse and handle uuids: {0}")]
|
||||
UUIDError(#[from] uuid::Error),
|
||||
|
||||
/// Wrapper around [`interpulse::Error`] to handle interpulse errors.
|
||||
#[error("failed to parse and handle metadata: {0}")]
|
||||
MetadataError(#[from] interpulse::Error),
|
||||
|
||||
/// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors.
|
||||
#[error("failed to manage serialization of json: {0}")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
|
||||
/// Wrapper around [`serde_ini::de::Error`] to handle Serde INI parsing errors.
|
||||
#[error("failed to manage deserialization of ini: {0}")]
|
||||
INIError(#[from] serde_ini::de::Error),
|
||||
|
||||
/// Wrapper around [`reqwest::Error`] to handle HTTP errors.
|
||||
#[error("failed to establish an http connection: {0}")]
|
||||
HTTPError(#[from] reqwest::Error),
|
||||
|
||||
/// Wrapper around [`zip::result::ZipError`] to handle zip errors.
|
||||
#[error("failed to manage zip files: {0}")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
|
||||
/// Wrapper around [`async_zip::error::ZipError`] to handle async zip errors.
|
||||
#[error("failed to manage zip files asynchronously: {0}")]
|
||||
AsyncZipError(#[from] async_zip::error::ZipError),
|
||||
|
||||
/// Wrapper around [`anyhow::Error`] to handle generic errors.
|
||||
#[error(transparent)]
|
||||
AnyhowError(#[from] anyhow::Error),
|
||||
|
||||
/// Wrapper around [`chrono::ParseError`] to handle date parsing errors.
|
||||
#[error("failed to parse and handle datetime: {0}")]
|
||||
ChronoError(#[from] chrono::ParseError),
|
||||
|
||||
/// Wrapper around [`crate::state::DirectoryError`] to handle directory errors.
|
||||
#[error("failed to get directory: {0}")]
|
||||
DirectoryError(#[from] crate::store::DirectoryError),
|
||||
|
||||
/// Wrapper around [`crate::api::proxy::ProxyError`] to handle ingress errors.
|
||||
#[error("failed to handle api communication: {0}")]
|
||||
ProxyError(#[from] crate::api::proxy::ProxyError),
|
||||
|
||||
/// Wrapper around [`crate::store::MinecraftAuthError`] to handle Minecraft auth errors.
|
||||
#[error("failed to authenticate with microsoft: {0}")]
|
||||
AuthError(#[from] crate::store::MinecraftAuthError),
|
||||
|
||||
/// Wrapper around [`crate::api::cluster::create::CreateClusterError`] to handle Cluster creation errors.
|
||||
#[error("failed to create clusters: {0}")]
|
||||
CreateClusterError(#[from] crate::api::cluster::create::CreateClusterError),
|
||||
|
||||
/// Wrapper around [`notify:Error`] to handle file watching errors.
|
||||
#[error("failed to watch file {0}")]
|
||||
NotifyError(#[from] notify::Error),
|
||||
|
||||
/// Wrapper around [`regex::Error`] to handle `RegExp` errors.
|
||||
#[error("string verification with regex failed: {0}")]
|
||||
RegexError(#[from] regex::Error),
|
||||
|
||||
/// Wrapper around [`reqwest::header::ToStrError`] to handle header conversion errors.
|
||||
#[error("failed to convert header to string: {0}")]
|
||||
HeaderToStrError(#[from] reqwest::header::ToStrError),
|
||||
|
||||
/// Wrapper around [`async_tungstenite::tungstenite::Error`] to handle WebSocket errors.
|
||||
#[error("failed to handle websocket connection: {0}")]
|
||||
WebSocketError(#[from] async_tungstenite::tungstenite::Error),
|
||||
|
||||
/// Indicates an error with SHA checksum.
|
||||
#[error("checksum {0} did not match {1}!")]
|
||||
HashError(String, String),
|
||||
|
||||
/// Wrapper around [`onelauncher_utils::io::IOError`] to handle wrapped `std::io` errors.
|
||||
#[error("error handling IO operations: {0}")]
|
||||
IOError(#[from] onelauncher_utils::io::IOError),
|
||||
|
||||
/// Wrapper around [`std::io::Error`] to handle non-wrapped `std::io` errors.
|
||||
#[error("error handling standard IO operations: {0}")]
|
||||
StdIOError(#[from] std::io::Error),
|
||||
|
||||
/// Wrapper around [`crate::utils::java::JavaError`] to handle java errors.
|
||||
#[error("failed to manage java installations: {0}")]
|
||||
JavaError(#[from] crate::utils::java::JavaError),
|
||||
|
||||
/// Wrapper around [`crate::store::StrongholdError`] to handle stronghold errors.
|
||||
#[error("failed to manage credentials with IOTA stronghold: {0}")]
|
||||
StrongholdError(#[from] crate::store::StrongholdError),
|
||||
|
||||
/// Wrapper around [`url::ParseError`] to handle URL parsing errors
|
||||
#[error("failed to parse and handle URLs: {0}")]
|
||||
URLError(#[from] url::ParseError),
|
||||
|
||||
/// Wrapper around [`tauri::Error`] to handle Tauri errors when the feature flag is enabled
|
||||
#[cfg(feature = "tauri")]
|
||||
#[error("error handling tauri: {0}")]
|
||||
TauriError(#[from] tauri::Error),
|
||||
}
|
||||
|
||||
/// A safe, accessible error structure wrapper around [`ErrorKind`] for use with [`tracing_error`].
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
/// The raw, inner [`ErrorKind`], wrapped in an [`Arc`].
|
||||
pub raw: std::sync::Arc<ErrorKind>,
|
||||
/// The traced [`ErrorKind`] ([`tracing_error`]) to be parsed and logged.
|
||||
pub source: tracing_error::TracedError<std::sync::Arc<ErrorKind>>,
|
||||
}
|
||||
|
||||
impl From<Error> for String {
|
||||
/// Converts an [`Error`] into a [`String`] value.
|
||||
fn from(value: Error) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
/// Get the source for the inner traced [`Error`].
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source.source()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
/// Format the [`Error`] and output it into the formatter buffer.
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.source)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<ErrorKind>> From<E> for Error {
|
||||
/// Converts an [`ErrorKind`] error into a traced [`Error`].
|
||||
/// Useful for reducing error mapping and standardizing error types.
|
||||
fn from(source: E) -> Self {
|
||||
let error = Into::<ErrorKind>::into(source);
|
||||
let boxed_error = std::sync::Arc::new(error);
|
||||
|
||||
Self {
|
||||
raw: boxed_error.clone(),
|
||||
source: boxed_error.in_current_span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
/// Converts the current [`ErrorKind`] into a traced [`Error`].
|
||||
pub fn as_error(self) -> Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for a [`core::result::Result`] with the error type [`Error`].
|
||||
pub type Result<T> = anyhow::Result<T, Error>;
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
//! Programatic Minecraft argument parser which replaces variables in arguments
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::prelude::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
|
||||
use interpulse::api::modded::SidedDataEntry;
|
||||
use interpulse::utils::get_path_from_artifact;
|
||||
|
||||
use crate::constants::DUMMY_REPLACE_NEWLINE;
|
||||
use crate::store::{Memory, MinecraftCredentials, Resolution};
|
||||
use onelauncher_utils::io::IOError;
|
||||
use onelauncher_utils::platform::classpath_separator;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn java_arguments(
|
||||
arguments: Option<&[Argument]>,
|
||||
natives_path: &Path,
|
||||
libraries_path: &Path,
|
||||
classpaths: &str,
|
||||
version: &str,
|
||||
memory: Memory,
|
||||
custom_args: Vec<String>,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut parsed = Vec::new();
|
||||
if let Some(args) = arguments {
|
||||
parse_arguments(
|
||||
args,
|
||||
&mut parsed,
|
||||
|a| {
|
||||
parse_java_argument(
|
||||
a.to_string(),
|
||||
natives_path,
|
||||
libraries_path,
|
||||
classpaths,
|
||||
version,
|
||||
java_arch,
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
)?;
|
||||
} else {
|
||||
parsed.push(format!(
|
||||
"-Djava.library.path={}",
|
||||
onelauncher_utils::io::canonicalize(natives_path)
|
||||
.map_err(|_| anyhow::anyhow!(
|
||||
"specified natives path {} not found",
|
||||
natives_path.to_string_lossy()
|
||||
))?
|
||||
.to_string_lossy()
|
||||
));
|
||||
parsed.push("-cp".to_string());
|
||||
parsed.push(classpaths.to_string());
|
||||
}
|
||||
|
||||
parsed.push(format!("-Xmx{}M", memory.maximum));
|
||||
parsed.push(format!("-Xms{}M", memory.minimum));
|
||||
|
||||
for arg in custom_args {
|
||||
if !arg.is_empty() {
|
||||
parsed.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn minecraft_arguments(
|
||||
args: Option<&[Argument]>,
|
||||
legacy_args: Option<&str>,
|
||||
creds: &MinecraftCredentials,
|
||||
version: &str,
|
||||
asset_index: &str,
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: VersionType,
|
||||
resolution: Resolution,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
if let Some(args) = args {
|
||||
let mut parsed = Vec::new();
|
||||
parse_arguments(
|
||||
args,
|
||||
&mut parsed,
|
||||
|arg| {
|
||||
parse_minecraft_argument(
|
||||
arg,
|
||||
&creds.access_token,
|
||||
&creds.username,
|
||||
creds.id,
|
||||
version,
|
||||
asset_index,
|
||||
game_directory,
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
)?;
|
||||
|
||||
Ok(parsed)
|
||||
} else if let Some(legacy_args) = legacy_args {
|
||||
let mut parsed = Vec::new();
|
||||
for arg in legacy_args.split(' ') {
|
||||
parsed.push(parse_minecraft_argument(
|
||||
&arg.replace(' ', DUMMY_REPLACE_NEWLINE),
|
||||
&creds.access_token,
|
||||
&creds.username,
|
||||
creds.id,
|
||||
version,
|
||||
asset_index,
|
||||
game_directory,
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
)?);
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn processor_arguments<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
args: &[T],
|
||||
data: &HashMap<String, SidedDataEntry>,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut parsed = Vec::new();
|
||||
|
||||
for arg in args {
|
||||
let a = &arg.as_ref()[1..arg.as_ref().len() - 1];
|
||||
if arg.as_ref().starts_with('{') {
|
||||
if let Some(entry) = data.get(a) {
|
||||
parsed.push(if entry.client.starts_with('[') {
|
||||
get_library(
|
||||
libraries_path,
|
||||
&entry.client[1..entry.client.len() - 1],
|
||||
true,
|
||||
)?
|
||||
} else {
|
||||
entry.client.clone()
|
||||
});
|
||||
}
|
||||
} else if arg.as_ref().starts_with('[') {
|
||||
parsed.push(get_library(libraries_path, a, true)?);
|
||||
} else {
|
||||
parsed.push(arg.as_ref().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub async fn main_class(path: String) -> crate::Result<Option<String>> {
|
||||
let main_class = tokio::task::spawn_blocking(move || {
|
||||
let zipfs = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut archive = zip::ZipArchive::new(zipfs)
|
||||
.map_err(|_| anyhow::anyhow!("failed to read main class {}", path))?;
|
||||
let file = archive
|
||||
.by_name("META-INF/MANIFEST.MF")
|
||||
.map_err(|_| anyhow::anyhow!("failed to read manifest {}", path))?;
|
||||
|
||||
let zip_reader = BufReader::new(file);
|
||||
for line in zip_reader.lines() {
|
||||
let mut line = line.map_err(IOError::from)?;
|
||||
line.retain(|c| !c.is_whitespace());
|
||||
|
||||
if line.starts_with("Main-Class:") {
|
||||
if let Some(class) = line.split(':').nth(1) {
|
||||
return Ok(Some(class.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<Option<String>, crate::Error>(None)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(main_class)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn parse_minecraft_argument(
|
||||
argument: &str,
|
||||
access_token: &str,
|
||||
username: &str,
|
||||
uuid: uuid::Uuid,
|
||||
version: &str,
|
||||
asset_index: &str,
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: VersionType,
|
||||
resolution: Resolution,
|
||||
) -> crate::Result<String> {
|
||||
Ok(argument
|
||||
.replace("${accessToken}", access_token)
|
||||
.replace("${auth_access_token}", access_token)
|
||||
.replace("${auth_session}", access_token)
|
||||
.replace("${auth_player_name}", username)
|
||||
.replace("${auth_xuid}", "0") // TODO: add auth xuid
|
||||
.replace("${auth_uuid}", &uuid.simple().to_string())
|
||||
.replace("${uuid}", &uuid.simple().to_string())
|
||||
.replace("${clientid}", crate::constants::MICROSOFT_CLIENT_ID)
|
||||
.replace("${user_properties}", "{}")
|
||||
.replace("${user_type}", "msa")
|
||||
.replace("${version_name}", version)
|
||||
.replace("${assets_index_name}", asset_index)
|
||||
.replace(
|
||||
"${game_directory}",
|
||||
&onelauncher_utils::io::canonicalize(game_directory)?.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${assets_root}",
|
||||
&onelauncher_utils::io::canonicalize(assets_directory)?.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${game_assets}",
|
||||
&onelauncher_utils::io::canonicalize(assets_directory)?.to_string_lossy(),
|
||||
)
|
||||
.replace("${version_type}", version_type.as_str())
|
||||
.replace("${resolution_width}", &resolution.0.to_string())
|
||||
.replace("${resolution_height}", &resolution.1.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn parse_java_argument(
|
||||
mut argument: String,
|
||||
natives_path: &Path,
|
||||
libraries_path: &Path,
|
||||
classpaths: &str,
|
||||
version: &str,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<String> {
|
||||
argument.retain(|c| !c.is_whitespace());
|
||||
Ok(argument
|
||||
.replace(
|
||||
"${natives_directory}",
|
||||
&onelauncher_utils::io::canonicalize(natives_path)?.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${library_directory}",
|
||||
&onelauncher_utils::io::canonicalize(libraries_path)?.to_string_lossy(),
|
||||
)
|
||||
.replace("${classpath_separator}", classpath_separator(java_arch))
|
||||
.replace("${launcher_name}", crate::constants::NAME)
|
||||
.replace("${launcher_version}", crate::constants::VERSION)
|
||||
.replace("${version_name}", version)
|
||||
.replace("${classpath}", classpaths))
|
||||
}
|
||||
|
||||
pub fn classpaths(
|
||||
libraries_path: &Path,
|
||||
libraries: &[Library],
|
||||
client_path: &Path,
|
||||
java_arch: &str,
|
||||
updated: bool,
|
||||
) -> crate::Result<String> {
|
||||
let mut classpaths = libraries
|
||||
.iter()
|
||||
.filter_map(|lib| {
|
||||
if let Some(rules) = &lib.rules {
|
||||
if !crate::game::rules(rules, java_arch, updated) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !lib.include_in_classpath {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(get_library(libraries_path, &lib.name, false))
|
||||
})
|
||||
.collect::<Result<HashSet<_>, _>>()?;
|
||||
|
||||
classpaths.insert(
|
||||
onelauncher_utils::io::canonicalize(client_path)?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
Ok(classpaths
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(classpath_separator(java_arch)))
|
||||
}
|
||||
|
||||
pub fn get_classpath_library<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
libraries: &[T],
|
||||
java_arch: &str,
|
||||
) -> crate::Result<String> {
|
||||
let classpaths = libraries
|
||||
.iter()
|
||||
.map(|lib| get_library(libraries_path, lib.as_ref(), false))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(classpaths.join(classpath_separator(java_arch)))
|
||||
}
|
||||
|
||||
pub fn get_library(
|
||||
libraries_path: &Path,
|
||||
library: &str,
|
||||
error_exist: bool,
|
||||
) -> crate::Result<String> {
|
||||
let mut path = libraries_path.to_path_buf();
|
||||
path.push(get_path_from_artifact(library)?);
|
||||
|
||||
if !path.exists() && error_exist {
|
||||
return Ok(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
let path = &onelauncher_utils::io::canonicalize(&path)?;
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
fn parse_arguments<ParseFn>(
|
||||
args: &[Argument],
|
||||
parsed: &mut Vec<String>,
|
||||
parse_function: ParseFn,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
ParseFn: Fn(&str) -> crate::Result<String>,
|
||||
{
|
||||
for arg in args {
|
||||
match arg {
|
||||
Argument::Normal(a) => {
|
||||
let p = parse_function(&a.replace(' ', DUMMY_REPLACE_NEWLINE))?;
|
||||
for split_p in p.split(DUMMY_REPLACE_NEWLINE) {
|
||||
parsed.push(split_p.to_string());
|
||||
}
|
||||
}
|
||||
Argument::Ruled { rules, value } => {
|
||||
if super::rules(rules, java_arch, true) {
|
||||
match value {
|
||||
ArgumentValue::Single(arg) => {
|
||||
parsed.push(parse_function(&arg.replace(' ', DUMMY_REPLACE_NEWLINE))?);
|
||||
}
|
||||
ArgumentValue::Many(args) => {
|
||||
for arg in args {
|
||||
parsed.push(parse_function(
|
||||
&arg.replace(' ', DUMMY_REPLACE_NEWLINE),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,349 +0,0 @@
|
|||
//! Downloads Minecraft metadata to be launched
|
||||
|
||||
use futures::prelude::*;
|
||||
use interpulse as ip;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use interpulse::api::minecraft::{
|
||||
Asset, AssetsIndex, Library, Os, Version as GameVersion, VersionInfo as GameVersionInfo,
|
||||
};
|
||||
use interpulse::api::modded::LoaderVersion;
|
||||
|
||||
use crate::proxy::send::send_ingress;
|
||||
use crate::proxy::utils::ingress_try_for_each;
|
||||
use crate::proxy::IngressId;
|
||||
use crate::store::State;
|
||||
use crate::utils::http::{fetch, fetch_json, write};
|
||||
use onelauncher_utils::io;
|
||||
use onelauncher_utils::platform::OsExt;
|
||||
|
||||
#[tracing::instrument(skip(st, version))]
|
||||
pub async fn download_minecraft(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
ingress: &IngressId,
|
||||
java_arch: &str,
|
||||
force: bool,
|
||||
updated: bool,
|
||||
) -> crate::Result<()> {
|
||||
tracing::info!("downloading minecraft version {}", version.id);
|
||||
let assets_index = download_assets_index(st, version, Some(ingress), force).await?;
|
||||
let processors = if version.processors.as_ref().is_some_and(|p| !p.is_empty()) {
|
||||
25.0
|
||||
} else {
|
||||
40.0
|
||||
};
|
||||
|
||||
tokio::try_join! {
|
||||
download_client(st, version, Some(ingress), force),
|
||||
download_assets(st, version.assets == "legacy", &assets_index, Some(ingress), processors, force),
|
||||
download_libraries(st, version.libraries.as_slice(), &version.id, Some(ingress), processors, java_arch, force, updated)
|
||||
}?;
|
||||
|
||||
tracing::info!("downloaded minecraft version {}", version.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn download_version_info(
|
||||
st: &State,
|
||||
version: &GameVersion,
|
||||
loader: Option<&LoaderVersion>,
|
||||
force: Option<bool>,
|
||||
ingress: Option<&IngressId>,
|
||||
) -> crate::Result<GameVersionInfo> {
|
||||
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
||||
tracing::debug!("loading minecraft version info for minecraft version {version_id}");
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(&version_id)
|
||||
.await
|
||||
.join(format!("{version_id}.json"));
|
||||
let result = if path.exists() && !force.unwrap_or(false) {
|
||||
io::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
tracing::info!(
|
||||
"downloading minecraft version info for minecraft version {}",
|
||||
&version_id
|
||||
);
|
||||
let mut info = fetch_json(
|
||||
reqwest::Method::GET,
|
||||
&version.url,
|
||||
None,
|
||||
None,
|
||||
&st.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(loader) = loader {
|
||||
let partial: ip::api::modded::PartialVersionInfo = fetch_json(
|
||||
reqwest::Method::GET,
|
||||
&loader.url,
|
||||
None,
|
||||
None,
|
||||
&st.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
info = ip::api::modded::merge_partial_version(partial, info);
|
||||
}
|
||||
|
||||
info.id.clone_from(&version_id);
|
||||
|
||||
write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?;
|
||||
Ok(info)
|
||||
}?;
|
||||
|
||||
if let Some(ingress) = ingress {
|
||||
send_ingress(ingress, 5.0, None).await?;
|
||||
}
|
||||
|
||||
tracing::debug!("loaded minecraft version info for minecraft version {version_id}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn download_assets_index(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
ingress: Option<&IngressId>,
|
||||
force: bool,
|
||||
) -> crate::Result<AssetsIndex> {
|
||||
tracing::debug!("loading assets index");
|
||||
let path = st
|
||||
.directories
|
||||
.index_dir()
|
||||
.await
|
||||
.join(format!("{}.json", &version.asset_index.id));
|
||||
|
||||
let result = if path.exists() && !force {
|
||||
io::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
} else {
|
||||
let index = fetch_json(
|
||||
reqwest::Method::GET,
|
||||
&version.asset_index.url,
|
||||
None,
|
||||
None,
|
||||
&st.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
write(&path, &serde_json::to_vec(&index)?, &st.io_semaphore).await?;
|
||||
tracing::info!("downloaded assets index");
|
||||
Ok(index)
|
||||
}?;
|
||||
|
||||
if let Some(ingress) = ingress {
|
||||
send_ingress(ingress, 5.0, None).await?;
|
||||
}
|
||||
|
||||
tracing::debug!("loaded assets index");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, index))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn download_assets(
|
||||
st: &State,
|
||||
legacy: bool,
|
||||
index: &AssetsIndex,
|
||||
ingress: Option<&IngressId>,
|
||||
ingress_amount: f64,
|
||||
force: bool,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("loading minecraft assets");
|
||||
let num_futs = index.objects.len();
|
||||
let assets = stream::iter(index.objects.iter()).map(Ok::<(&String, &Asset), crate::Error>);
|
||||
|
||||
ingress_try_for_each(
|
||||
assets,
|
||||
None,
|
||||
ingress,
|
||||
ingress_amount,
|
||||
num_futs,
|
||||
None,
|
||||
|(name, asset)| async move {
|
||||
let hash = &asset.hash;
|
||||
let resources = st.directories.object_dir(hash).await;
|
||||
let url = format!(
|
||||
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
|
||||
sub_hash = &hash[..2]
|
||||
);
|
||||
let fetch_cell = OnceCell::<bytes::Bytes>::new();
|
||||
tokio::try_join! {
|
||||
async {
|
||||
if !resources.exists() || force {
|
||||
let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
|
||||
write(&resources, resource, &st.io_semaphore).await?;
|
||||
tracing::trace!("fetched asset resource with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
async {
|
||||
let resources = st.directories.legacy_assets_dir().await.join(
|
||||
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
||||
);
|
||||
|
||||
if legacy && !resources.exists() || force {
|
||||
let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
|
||||
write(&resources, resource, &st.io_semaphore).await?;
|
||||
tracing::trace!("fetched legacy asset resource with hash {hash}");
|
||||
}
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
}?;
|
||||
|
||||
tracing::trace!("loaded asset resource with hash {hash}");
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::debug!("loaded minecraft assets");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, libraries))]
|
||||
#[onelauncher_macros::memory]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn download_libraries(
|
||||
st: &State,
|
||||
libraries: &[Library],
|
||||
version: &str,
|
||||
ingress: Option<&IngressId>,
|
||||
ingress_amount: f64,
|
||||
java_arch: &str,
|
||||
force: bool,
|
||||
updated: bool,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("loading minecraft libraries");
|
||||
|
||||
tokio::try_join! {
|
||||
io::create_dir_all(st.directories.libraries_dir().await),
|
||||
io::create_dir_all(st.directories.version_natives_dir(version).await)
|
||||
}?;
|
||||
|
||||
let num_files = libraries.len();
|
||||
// TODO: figure out why rustfmt won't format this (it's not a macro...)
|
||||
ingress_try_for_each(
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>),
|
||||
None,
|
||||
ingress,
|
||||
ingress_amount,
|
||||
num_files,
|
||||
None,
|
||||
|lib| async move {
|
||||
if let Some(rules) = &lib.rules {
|
||||
if !super::rules(rules, java_arch, updated) {
|
||||
tracing::trace!("skipped library {} due to rule", &lib.name);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if !lib.downloadable {
|
||||
tracing::trace!("skipped non-downloadable library {}", &lib.name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = ip::utils::get_path_from_artifact(&lib.name)?;
|
||||
let path = st.directories.libraries_dir().await.join(&artifact_path);
|
||||
|
||||
if path.exists() && !force {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(ip::api::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact), ..
|
||||
}) = lib.downloads {
|
||||
if !artifact.url.is_empty() {
|
||||
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 {:?}", &lib.name, &path);
|
||||
return Ok::<_, crate::Error>(());
|
||||
}
|
||||
}
|
||||
|
||||
let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
|
||||
let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
async {
|
||||
if let Some((os_key, classifiers)) = None.or_else(|| Some((
|
||||
lib.natives.as_ref()?.get(&Os::native_arch(java_arch))?,
|
||||
lib.downloads.as_ref()?.classifiers.as_ref()?
|
||||
))) {
|
||||
let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH);
|
||||
if let Some(native) = classifiers.get(&parsed) {
|
||||
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) {
|
||||
Ok(()) => tracing::debug!("downloaded native {}", &lib.name),
|
||||
Err(err) => tracing::error!("failed to download native {}: {}", &lib.name, err)
|
||||
}
|
||||
} else {
|
||||
tracing::error!("failed to extract native {}", &lib.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}?;
|
||||
|
||||
tracing::debug!("loaded minecraft library {}", lib.name);
|
||||
Ok(())
|
||||
}
|
||||
).await?;
|
||||
|
||||
tracing::debug!("downloaded all minecraft libraries");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn download_client(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
ingress: Option<&IngressId>,
|
||||
force: bool,
|
||||
) -> crate::Result<()> {
|
||||
let version_id = &version.id;
|
||||
tracing::debug!("loading minecraft client for minecraft version {version_id}");
|
||||
|
||||
let client = version
|
||||
.downloads
|
||||
.get(&ip::api::minecraft::DownloadType::Client)
|
||||
.ok_or_else(|| anyhow::anyhow!("no client downloads exist for {version_id}"))?;
|
||||
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version_id)
|
||||
.await
|
||||
.join(format!("{version_id}.jar"));
|
||||
|
||||
if !path.exists() || force {
|
||||
let result = fetch(&client.url, Some(&client.sha1), &st.fetch_semaphore).await?;
|
||||
|
||||
write(&path, &result, &st.io_semaphore).await?;
|
||||
tracing::trace!("fetched minecraft client version {version_id}");
|
||||
}
|
||||
|
||||
if let Some(ingress) = ingress {
|
||||
send_ingress(ingress, 9.0, None).await?;
|
||||
}
|
||||
|
||||
tracing::debug!("downloaded client for minecraft version {version_id}");
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,544 +0,0 @@
|
|||
//! **`OneLauncher` Game**
|
||||
//!
|
||||
//! Manages, installs, and launches the core Minecraft game.
|
||||
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::processor;
|
||||
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
|
||||
use crate::proxy::{IngressId, IngressType};
|
||||
use crate::store::{
|
||||
self as st, Cluster, ClusterStage, MinecraftCredentials, ProcessorChild, State,
|
||||
};
|
||||
use onelauncher_utils::io::{self, IOError};
|
||||
|
||||
use chrono::Utc;
|
||||
use interpulse as ip;
|
||||
use interpulse::api::minecraft::{RuleAction, VersionInfo};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod arguments;
|
||||
pub mod metadata;
|
||||
|
||||
macro_rules! data_entry {
|
||||
($dest:expr; $($name:literal: client => $client:expr, server => $server:expr;)+) => {
|
||||
$(std::collections::HashMap::insert(
|
||||
$dest,
|
||||
String::from($name),
|
||||
interpulse::api::modded::SidedDataEntry {
|
||||
client: String::from($client),
|
||||
server: String::from($server),
|
||||
},
|
||||
);)+
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(cluster))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn install_minecraft(
|
||||
cluster: &Cluster,
|
||||
old_ingress: Option<IngressId>,
|
||||
repair: bool,
|
||||
) -> crate::Result<()> {
|
||||
// let should_sync = old_ingress.is_some();
|
||||
let ingress = init_or_edit_ingress(
|
||||
old_ingress,
|
||||
IngressType::DownloadLoader {
|
||||
cluster_name: cluster.meta.name.clone(),
|
||||
cluster_path: cluster.get_full_path().await?,
|
||||
},
|
||||
100.0,
|
||||
"downloading minecraft loader",
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::api::cluster::edit(&cluster.cluster_path(), |clus| {
|
||||
clus.stage = ClusterStage::Downloading;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
State::sync().await?;
|
||||
|
||||
// if should_sync {
|
||||
// let path = cluster.cluster_path();
|
||||
// tokio::task::spawn(async move {
|
||||
// Cluster::sync_packages(&path).await
|
||||
// });
|
||||
// }
|
||||
|
||||
let state = State::get().await?;
|
||||
let instance_path = &io::canonicalize(cluster.get_full_path().await?)?;
|
||||
let metadata = state.metadata.read().await;
|
||||
|
||||
let version_manifest = metadata.minecraft.clone();
|
||||
let versions = version_manifest
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft manifest"))?
|
||||
.versions;
|
||||
|
||||
let version_idx = versions
|
||||
.iter()
|
||||
.position(|g| g.id == cluster.meta.mc_version)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("invalid minecraft game version {}", cluster.meta.mc_version)
|
||||
})?;
|
||||
|
||||
let version = &versions[version_idx];
|
||||
let updated = version_idx <= versions.iter().position(|g| g.id == "22w16a").unwrap_or(0); // LWJGL patching
|
||||
let version_jar = cluster
|
||||
.meta
|
||||
.loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |g| {
|
||||
format!("{}-{}", version.id.clone(), g.id.clone())
|
||||
});
|
||||
let mut version_info = metadata::download_version_info(
|
||||
&state,
|
||||
version,
|
||||
cluster.meta.loader_version.as_ref(),
|
||||
Some(repair),
|
||||
Some(&ingress),
|
||||
)
|
||||
.await?;
|
||||
// TODO: java checking and installation
|
||||
let key = version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.map_or(8, |it| it.major_version);
|
||||
let (java_version, set_java) =
|
||||
if let Some(java_version) = java_version_from_cluster(cluster, &version_info).await? {
|
||||
(std::path::PathBuf::from(java_version.path), false)
|
||||
} else {
|
||||
(crate::api::java::install_java_from_major(key).await?, true)
|
||||
};
|
||||
|
||||
let java_version = crate::api::java::check_java(java_version.clone())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("java path validation failed: {:?}", java_version))?;
|
||||
|
||||
if set_java {
|
||||
{
|
||||
let mut settings = state.settings.write().await;
|
||||
settings
|
||||
.java_versions
|
||||
.insert(key.to_string(), java_version.clone());
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
metadata::download_minecraft(
|
||||
&state,
|
||||
&version_info,
|
||||
&ingress,
|
||||
&java_version.arch,
|
||||
repair,
|
||||
updated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(processors) = &version_info.processors {
|
||||
let client = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.await
|
||||
.join(format!("{version_jar}.jar"));
|
||||
let libraries = state.directories.libraries_dir().await;
|
||||
if let Some(ref mut data) = version_info.data {
|
||||
data_entry! {
|
||||
data;
|
||||
"SIDE":
|
||||
client => "client",
|
||||
server => "";
|
||||
"MINECRAFT_JAR":
|
||||
client => client.to_string_lossy(),
|
||||
server => "";
|
||||
"MINECRAFT_VERSION":
|
||||
client => cluster.meta.mc_version.clone(),
|
||||
server => "";
|
||||
"ROOT":
|
||||
client => instance_path.to_string_lossy(),
|
||||
server => "";
|
||||
"LIBRARY_DIR":
|
||||
client => libraries.to_string_lossy(),
|
||||
server => "";
|
||||
}
|
||||
|
||||
send_ingress(&ingress, 0.0, Some("running forge processors")).await?;
|
||||
let total_length = processors.len();
|
||||
for (index, processor) in processors.iter().enumerate() {
|
||||
if let Some(sides) = &processor.sides {
|
||||
if !sides.contains(&String::from("client")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let cp = onelauncher_utils::ref_owned!(cp = processor.classpath.clone() => {
|
||||
cp.push(processor.jar.clone());
|
||||
});
|
||||
|
||||
let child = Command::new(&java_version.path)
|
||||
.arg("-cp")
|
||||
.arg(arguments::get_classpath_library(
|
||||
&libraries,
|
||||
&cp,
|
||||
&java_version.arch,
|
||||
)?)
|
||||
.arg(
|
||||
arguments::main_class(arguments::get_library(
|
||||
&libraries,
|
||||
&processor.jar,
|
||||
false,
|
||||
)?)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"failed to find processor main class for {}",
|
||||
processor.jar
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.args(arguments::processor_arguments(
|
||||
&libraries,
|
||||
&processor.args,
|
||||
data,
|
||||
)?)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &java_version.path))
|
||||
.map_err(|err| anyhow::anyhow!("failed to run processor: {err}"))?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"error occured while running processor: {}",
|
||||
String::from_utf8_lossy(&child.stderr)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
send_ingress(
|
||||
&ingress,
|
||||
30.0 / total_length as f64,
|
||||
Some(&format!("running forge processor {index}/{total_length}")),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::api::cluster::edit(&cluster.cluster_path(), |clus| {
|
||||
clus.stage = ClusterStage::Installed;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
send_ingress(&ingress, 100.0, Some("installed minecraft successfully")).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[onelauncher_macros::memory]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn launch_minecraft(
|
||||
cluster: &Cluster,
|
||||
java_args: &[String],
|
||||
env_args: &[(String, String)],
|
||||
mc_options: &[(String, String)],
|
||||
post_hook: Option<String>,
|
||||
credentials: &MinecraftCredentials,
|
||||
resolution: &st::Resolution,
|
||||
memory: &st::Memory,
|
||||
wrapper: &Option<String>,
|
||||
) -> crate::Result<Arc<tokio::sync::RwLock<ProcessorChild>>> {
|
||||
if cluster.stage == ClusterStage::PackDownloading || cluster.stage == ClusterStage::Downloading
|
||||
{
|
||||
return Err(anyhow::anyhow!("cluster is still downloading").into());
|
||||
}
|
||||
|
||||
if cluster.stage != ClusterStage::Installed {
|
||||
install_minecraft(cluster, None, false).await?;
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let metadata = state.metadata.read().await;
|
||||
let instance_path = cluster.get_full_path().await?;
|
||||
let instance_path = &io::canonicalize(instance_path)?;
|
||||
|
||||
let version_manifest = metadata.minecraft.clone();
|
||||
let versions = version_manifest
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft manifest"))?
|
||||
.versions;
|
||||
|
||||
let version_index = versions
|
||||
.iter()
|
||||
.position(|it| it.id == cluster.meta.mc_version)
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid game version {}", cluster.meta.mc_version))?;
|
||||
|
||||
let version = &versions[version_index];
|
||||
let updated = version_index <= versions.iter().position(|x| x.id == "22w16a").unwrap_or(0);
|
||||
let version_jar = cluster
|
||||
.meta
|
||||
.loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |it| {
|
||||
format!("{}-{}", version.id.clone(), it.id.clone())
|
||||
});
|
||||
let version_info = metadata::download_version_info(
|
||||
&state,
|
||||
version,
|
||||
cluster.meta.loader_version.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let java_version = java_version_from_cluster(cluster, &version_info)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("missing java installation"))?;
|
||||
let java_version = crate::api::java::check_java(java_version.path.clone().into())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("java path invalid: {}", java_version.path))?;
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.await
|
||||
.join(format!("{version_jar}.jar"));
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
let mut command = match wrapper {
|
||||
Some(hook) => {
|
||||
onelauncher_utils::ref_owned!(it = Command::new(hook) => {it.arg(&java_version.path)})
|
||||
}
|
||||
None => Command::new(&java_version.path),
|
||||
};
|
||||
let env_args = Vec::from(env_args);
|
||||
if !state.settings.read().await.allow_parallel_running_clusters {
|
||||
let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?;
|
||||
if let Some(uuid) = existing.first() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} is already running ({uuid})",
|
||||
cluster.cluster_path()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
command
|
||||
.args(
|
||||
arguments::java_arguments(
|
||||
args.get(&ip::api::minecraft::ArgumentType::Jvm)
|
||||
.map(Vec::as_slice),
|
||||
&state.directories.version_natives_dir(&version_jar).await,
|
||||
&state.directories.libraries_dir().await,
|
||||
&arguments::classpaths(
|
||||
&state.directories.libraries_dir().await,
|
||||
version_info.libraries.as_slice(),
|
||||
&client_path,
|
||||
&java_version.arch,
|
||||
updated,
|
||||
)?,
|
||||
&version_jar,
|
||||
*memory,
|
||||
Vec::from(java_args),
|
||||
&java_version.arch,
|
||||
)?
|
||||
.into_iter(),
|
||||
)
|
||||
.arg(version_info.main_class.clone())
|
||||
.args(
|
||||
arguments::minecraft_arguments(
|
||||
args.get(&ip::api::minecraft::ArgumentType::Game)
|
||||
.map(Vec::as_slice),
|
||||
version_info.minecraft_arguments.as_deref(),
|
||||
credentials,
|
||||
&version.id,
|
||||
&version_info.asset_index.id,
|
||||
instance_path,
|
||||
&state.directories.assets_dir().await,
|
||||
version.type_,
|
||||
*resolution,
|
||||
&java_version.arch,
|
||||
)?
|
||||
.into_iter(),
|
||||
)
|
||||
.current_dir(instance_path.clone());
|
||||
|
||||
// when cargo sets the `DYLD_LIBRARY_PATH`` it breaks Minecraft
|
||||
#[cfg(target_os = "macos")]
|
||||
if std::env::var("CARGO").is_ok() {
|
||||
command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
|
||||
}
|
||||
|
||||
// remove pre-existing java options, as they should be set in the cluster settings.
|
||||
command.env_remove("_JAVA_OPTIONS");
|
||||
command.envs(env_args);
|
||||
|
||||
// overrides `options.txt` with our settings: i can't believe it's not yaml
|
||||
if !mc_options.is_empty() {
|
||||
let options_path = instance_path.join("options.txt");
|
||||
let mut options_string = if options_path.exists() {
|
||||
io::read_to_string(&options_path).await?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
for (key, value) in mc_options {
|
||||
let re = regex::Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
|
||||
if re.is_match(&options_string) {
|
||||
let replaced_string = re
|
||||
.replace_all(&options_string, &format!("{key}:{value}"))
|
||||
.to_string();
|
||||
options_string = replaced_string;
|
||||
} else {
|
||||
options_string.push_str(&format!("\n{key}:{value}"));
|
||||
}
|
||||
}
|
||||
|
||||
io::write(&options_path, options_string).await?;
|
||||
}
|
||||
|
||||
crate::api::cluster::edit(&cluster.cluster_path(), |clust| {
|
||||
clust.meta.played_at = Some(Utc::now());
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
let mut censors = HashMap::new();
|
||||
let username = whoami::username();
|
||||
let realname = whoami::realname();
|
||||
censors.insert(format!("/{username}/"), "/{ENV_USERNAME}/".to_string());
|
||||
censors.insert(format!("\\{username}\\"), "\\{ENV_USERNAME}\\".to_string());
|
||||
censors.insert(format!("/{realname}/"), "/{ENV_REALNAME}/".to_string());
|
||||
censors.insert(format!("\\{realname}\\"), "\\{ENV_REALNAME}\\".to_string());
|
||||
censors.insert(
|
||||
credentials.access_token.clone(),
|
||||
"{MC_ACCESS_TOKEN}".to_string(),
|
||||
);
|
||||
censors.insert(credentials.username.clone(), "{MC_USERNAME}".to_string());
|
||||
censors.insert(
|
||||
credentials.id.as_simple().to_string(),
|
||||
"{MC_UUID}".to_string(),
|
||||
);
|
||||
censors.insert(
|
||||
credentials.id.as_hyphenated().to_string(),
|
||||
"{MC_UUID}".to_string(),
|
||||
);
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
use crate::ProxyState;
|
||||
let window = ProxyState::get_main_window().await?;
|
||||
let settings = state.settings.read().await;
|
||||
if settings.hide_on_launch {
|
||||
window.minimize()?;
|
||||
}
|
||||
}
|
||||
|
||||
if !*state.offline.read().await {
|
||||
let _ = state
|
||||
.discord_rpc
|
||||
.set_activity(&format!("Playing {}", cluster.meta.name), true)
|
||||
.await;
|
||||
}
|
||||
|
||||
let enable_gamemode = state.settings.read().await.enable_gamemode;
|
||||
let mut state_processor = state.processor.write().await;
|
||||
state_processor
|
||||
.insert_process(
|
||||
Uuid::new_v4(),
|
||||
cluster.cluster_path(),
|
||||
command,
|
||||
post_hook,
|
||||
censors,
|
||||
Some(credentials.id),
|
||||
Some(enable_gamemode),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Parses an array of Minecraft library feature or OS rules.
|
||||
#[tracing::instrument]
|
||||
pub fn rules(rules: &[ip::api::minecraft::Rule], java_version: &str, updated: bool) -> bool {
|
||||
let mut rule = rules
|
||||
.iter()
|
||||
.map(|r| rule(r, java_version, updated))
|
||||
.collect::<Vec<Option<bool>>>();
|
||||
if rules
|
||||
.iter()
|
||||
.all(|r| matches!(r.action, RuleAction::Disallow))
|
||||
{
|
||||
rule.push(Some(true));
|
||||
}
|
||||
|
||||
!(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(Option::is_none))
|
||||
}
|
||||
|
||||
/// Parses a Minecraft library feature or OS rule.
|
||||
/// Is disallowed -> Don't include it
|
||||
/// Is not allowed -> Don't include it
|
||||
/// Is allowed -> Include it
|
||||
#[tracing::instrument]
|
||||
pub fn rule(rule: &ip::api::minecraft::Rule, java_version: &str, updated: bool) -> Option<bool> {
|
||||
use ip::api::minecraft::{Rule, RuleAction};
|
||||
|
||||
let result = match rule {
|
||||
Rule {
|
||||
os: Some(ref os), ..
|
||||
} => onelauncher_utils::platform::os_rule(os, java_version, updated),
|
||||
Rule {
|
||||
features: Some(ref features),
|
||||
..
|
||||
} => {
|
||||
!features.is_demo_user.unwrap_or(true)
|
||||
|| features.has_custom_resolution.unwrap_or(false)
|
||||
|| !features.has_quick_plays_support.unwrap_or(true)
|
||||
|| !features.is_quick_play_multiplayer.unwrap_or(true)
|
||||
|| !features.is_quick_play_realms.unwrap_or(true)
|
||||
|| !features.is_quick_play_singleplayer.unwrap_or(true)
|
||||
}
|
||||
_ => return Some(true),
|
||||
};
|
||||
|
||||
match rule.action {
|
||||
RuleAction::Allow => {
|
||||
if result {
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
}
|
||||
RuleAction::Disallow => {
|
||||
if result {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn java_version_from_cluster(
|
||||
cluster: &Cluster,
|
||||
version_info: &VersionInfo,
|
||||
) -> crate::Result<Option<JavaVersion>> {
|
||||
if let Some(java) = cluster.java.clone().and_then(|x| x.custom_version) {
|
||||
Ok(Some(java))
|
||||
} else {
|
||||
let key = version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.map_or(8, |x| x.major_version);
|
||||
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
|
||||
if let Some(j) = settings.java_versions.get(&key.to_string()) {
|
||||
return Ok(Some(j.clone()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#![allow(clippy::all)]
|
||||
//! # `OneLauncher`
|
||||
//!
|
||||
//! A library used as a core for our launcher and Rust APIs.
|
||||
|
||||
#[macro_use]
|
||||
pub mod utils;
|
||||
|
||||
pub mod api;
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod game;
|
||||
pub mod logger;
|
||||
pub mod store;
|
||||
|
||||
pub use api::proxy::{Ingress, IngressType, ProxyState};
|
||||
pub use api::*;
|
||||
pub use error::*;
|
||||
pub use logger::start_logger;
|
||||
pub use store::{InnerPathLinux, State};
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
//! **`OneLauncher` Logger**
|
||||
//!
|
||||
//! Public utilities for [`tracing`] logging in `OneLauncher`.
|
||||
//!
|
||||
//! - `RUST_LOG=xxx` is a variable which defines tracing configuration (`ERROR > WARN > INFO > DEBUG > TRACE`)
|
||||
//! - `RUST_LOG="onelauncher=trace"` will show **all** mesages from `OneLauncher` and not other dependencies.
|
||||
//! - `RUST_LOG="info"` will show info, warn, and error logs.
|
||||
//!
|
||||
//! Errors returned to [`tauri`] are displayed as tracing error logs.
|
||||
//! These errors also include an attached span trace if the error is from a [`tracing_error`] and the level is info, debug, or trace.
|
||||
//! Tracing errors also provide Sentry metadata to be handled.
|
||||
//! To set the environment variable on `unix` you can use the following command: `RUST_LOG="xxx" {your command}`
|
||||
//!
|
||||
//! The default `RUST_LOG` is `onelauncher=show`, meaning only logs from `OneLauncher` will be displayed at the info, warn, and error levels.
|
||||
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
|
||||
// TODO: refactor logging and add more env filters.
|
||||
|
||||
/// Initialize the global [`tracing`] logger in development.
|
||||
#[cfg(debug_assertions)]
|
||||
#[must_use]
|
||||
pub fn start_logger() -> Option<WorkerGuard> {
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
tracing_subscriber::EnvFilter::new("onelauncher=info,onelauncher_gui=info")
|
||||
});
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(filter)
|
||||
.with(tracing_error::ErrorLayer::default());
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Initialize the global [`tracing`] logger in production.
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn start_logger() -> Option<WorkerGuard> {
|
||||
use crate::store::Directories;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::fmt::time::ChronoLocal;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
let logs_dir = if let Some(directory) = Directories::logs_dir() {
|
||||
directory
|
||||
} else {
|
||||
eprintln!("failed to start logger");
|
||||
return None;
|
||||
};
|
||||
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("onelauncher=info"));
|
||||
|
||||
let file_appender = RollingFileAppender::new(Rotation::DAILY, logs_dir, "onelauncher.log");
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false)
|
||||
.with_timer(ChronoLocal::rfc_3339()),
|
||||
)
|
||||
.with(filter)
|
||||
.with(tracing_error::ErrorLayer::default());
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
Some(guard)
|
||||
}
|
||||
|
|
@ -1,802 +0,0 @@
|
|||
//! Core cluster state management
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::prelude::*;
|
||||
use interpulse::api::modded::LoaderVersion;
|
||||
use notify::RecommendedWatcher;
|
||||
use notify_debouncer_mini::Debouncer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::constants::CLUSTER_FILE;
|
||||
use crate::proxy::send::{send_cluster, send_message};
|
||||
use crate::proxy::ClusterPayloadType;
|
||||
use crate::utils::http::{write_icon, IoSemaphore};
|
||||
use crate::utils::java::JavaVersion;
|
||||
use crate::State;
|
||||
use onelauncher_utils::io::{self, IOError};
|
||||
|
||||
use super::{Directories, InitHooks, Memory, PackageType, Resolution};
|
||||
|
||||
/// Core Cluster state manager with a [`HashMap<ClusterPath, Cluster>`].
|
||||
pub struct Clusters(pub HashMap<ClusterPath, Cluster>);
|
||||
|
||||
/// Core Cluster stages used in package logic.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ClusterStage {
|
||||
/// Installed with no downloads left.
|
||||
Installed,
|
||||
/// Downloading any sort of core metadata or mod file.
|
||||
Downloading,
|
||||
/// Downloading a full pack file, which is granted lower priority due to the high network demand.
|
||||
PackDownloading,
|
||||
/// Not installed at all.
|
||||
#[default]
|
||||
NotInstalled,
|
||||
}
|
||||
|
||||
impl ClusterStage {
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
Self::Installed => "installed",
|
||||
Self::Downloading => "downloading",
|
||||
Self::PackDownloading => "pack_downloading",
|
||||
Self::NotInstalled => "not_installed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ClusterStage {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"installed" => Self::Installed,
|
||||
"downloading" => Self::Downloading,
|
||||
"pack_downloading" => Self::PackDownloading,
|
||||
_ => Self::NotInstalled,
|
||||
})
|
||||
}
|
||||
|
||||
type Err = crate::Error;
|
||||
}
|
||||
|
||||
/// Relative Path wrapper to be used as an identifer for a cluster path.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[serde(transparent)]
|
||||
pub struct ClusterPath(pub PathBuf);
|
||||
|
||||
impl ClusterPath {
|
||||
/// Get a new [`ClusterPath`] instance from an [`Into<PathBuf>`].
|
||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||
Self(path.into())
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: Uuid) -> crate::Result<Self> {
|
||||
Ok(crate::cluster::get_by_uuid(uuid)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Cluster does not exist"))?
|
||||
.cluster_path())
|
||||
}
|
||||
|
||||
/// Get the full [`PathBuf`] of the current cluster path.
|
||||
pub async fn full_path(&self) -> crate::Result<PathBuf> {
|
||||
let state: std::sync::Arc<tokio::sync::RwLockReadGuard<'_, State>> = State::get().await?;
|
||||
let clusters_dir = state.directories.clusters_dir().await;
|
||||
Ok(clusters_dir.join(&self.0))
|
||||
}
|
||||
|
||||
/// Get the full [`PathBuf`] of the current cluster path.
|
||||
pub async fn full_path_dirs(&self, dirs: &Directories) -> crate::Result<PathBuf> {
|
||||
let clusters_dir = dirs.clusters_dir().await;
|
||||
Ok(clusters_dir.join(&self.0))
|
||||
}
|
||||
|
||||
/// Validate the UTF of a cluster path.
|
||||
pub fn validate(&self) -> crate::Result<&Self> {
|
||||
self.0.to_str().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"invalid file path string {}!",
|
||||
self.0.clone().to_string_lossy()
|
||||
)
|
||||
})?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Validate the cluster and clone the current [`ClusterPath`].
|
||||
pub async fn cluster_path(&self) -> crate::Result<Self> {
|
||||
(crate::cluster::get(self).await?).map_or_else(
|
||||
|| {
|
||||
Err(anyhow::anyhow!(
|
||||
"failed to get path of unmanaged or corrupted cluster {}",
|
||||
self.to_string()
|
||||
)
|
||||
.into())
|
||||
},
|
||||
|c| Ok(c.cluster_path()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a [`ClusterPath`] from a full [`PathBuf`].
|
||||
pub async fn from_path(path: PathBuf) -> crate::Result<Self> {
|
||||
let path: PathBuf = io::canonicalize(path)?;
|
||||
let clusters_dir = io::canonicalize(State::get().await?.directories.clusters_dir().await)?;
|
||||
path.strip_prefix(clusters_dir)
|
||||
.ok()
|
||||
.and_then(|f| f.file_name())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("path {} is not a cluster path", path.to_string_lossy())
|
||||
})?;
|
||||
|
||||
Ok(Self(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ClusterPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.display().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for backwards compatibility for modpacks which handle windows paths strangely.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[serde(into = "RawPackagePath", from = "RawPackagePath")]
|
||||
pub struct InnerPathLinux(pub String);
|
||||
|
||||
impl InnerPathLinux {
|
||||
/// Get the first 2 components of the inner path.
|
||||
#[must_use]
|
||||
pub fn get_components(&self) -> String {
|
||||
self.to_string()
|
||||
.split('/')
|
||||
.take(2)
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InnerPathLinux {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RawPackagePath> for InnerPathLinux {
|
||||
fn from(value: RawPackagePath) -> Self {
|
||||
Self(value.0.replace('\\', "/"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for backwards compatibility for modpacks which handle windows paths strangely.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
struct RawPackagePath(pub String);
|
||||
|
||||
impl From<InnerPathLinux> for RawPackagePath {
|
||||
fn from(value: InnerPathLinux) -> Self {
|
||||
Self(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make it not a pathbuf, just get rid of all packagepath and clusterpath into str.
|
||||
|
||||
/// Relative [`PathBuf`] for a specific [`Package`] of a [`Cluster`].
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[serde(transparent)]
|
||||
pub struct PackagePath(PathBuf);
|
||||
|
||||
impl PackagePath {
|
||||
/// Transform a full [`PathBuf`] into a relative [`PackagePath`].
|
||||
pub async fn from_path(path: &PathBuf) -> crate::Result<Self> {
|
||||
let clusters_dir: PathBuf =
|
||||
std::fs::canonicalize(State::get().await?.directories.clusters_dir().await)?;
|
||||
let path: PathBuf = std::fs::canonicalize(path)?;
|
||||
let path = path
|
||||
.strip_prefix(clusters_dir)
|
||||
.ok()
|
||||
.map(|p| p.components().skip(1).collect::<PathBuf>())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("path {path:?} does not exist in a cluster!", path = path)
|
||||
})?;
|
||||
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
pub fn new(package_type: PackageType, file_name: String) -> crate::Result<Self> {
|
||||
Ok(Self(
|
||||
PathBuf::from_str(format!("{}/{}", package_type.get_folder(), file_name).as_str())
|
||||
.map_err(|e| anyhow::anyhow!("couldn't create package path from '{package_type}' '{file_name}': {e:?}"))?
|
||||
))
|
||||
}
|
||||
|
||||
/// Get the full [`PathBuf`] of the current package path.
|
||||
pub async fn full_path(&self, cluster: &ClusterPath) -> crate::Result<PathBuf> {
|
||||
let cluster_dir = cluster.full_path().await?;
|
||||
Ok(cluster_dir.join(&self.0))
|
||||
}
|
||||
|
||||
pub fn package_type(&self) -> crate::Result<PackageType> {
|
||||
PackageType::from_parent(&self.0).ok_or_else(|| anyhow::anyhow!("couldn't get package type of package path: {:?}", self.0).into())
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> crate::Result<String> {
|
||||
self.0
|
||||
.file_name()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get file name of package path: {:?}", self.0).into())
|
||||
}
|
||||
|
||||
pub fn relative_path(&self) -> PathBuf {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a single Instance and installation of Minecraft
|
||||
/// Contains settings and identifiers on a per-Cluster basis, falling back to default settings for Options<>
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Cluster {
|
||||
/// The Uuid of a specific cluster.
|
||||
pub uuid: Uuid,
|
||||
/// The download stage that of a cluster.
|
||||
#[serde(default)]
|
||||
pub stage: ClusterStage,
|
||||
/// The core path of the cluster.
|
||||
#[serde(default)]
|
||||
pub path: PathBuf,
|
||||
/// The associated cluster metadata.
|
||||
pub meta: ClusterMeta,
|
||||
/// The per-cluster JVM memory allocation options.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<Memory>,
|
||||
/// The per-cluster JVM options.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub java: Option<JavaOptions>,
|
||||
/// The per-cluster Minecraft window default resolution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<Resolution>,
|
||||
/// The per-cluster Minecraft window fullscreen status.
|
||||
pub force_fullscreen: Option<bool>,
|
||||
// The per-cluster initialization hooks.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub init_hooks: Option<InitHooks>,
|
||||
}
|
||||
|
||||
/// Represents core Cluster metadata ([`Cluster#meta`]).
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ClusterMeta {
|
||||
/// The user-facing name of the cluster stored as a string.
|
||||
pub name: String,
|
||||
|
||||
/// The user-facing group of the cluster stored as a string.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
/// The associated Minecraft version of the cluster as last updated.
|
||||
pub mc_version: String,
|
||||
|
||||
/// The associated mod [`Loader`] as specified in the cluster.
|
||||
#[serde(default)]
|
||||
pub loader: Loader,
|
||||
/// The associated mod [`LoaderVersion`] if available.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub loader_version: Option<LoaderVersion>,
|
||||
|
||||
/// The time that the cluster was created in [`DateTime<Utc>`].
|
||||
#[serde(default)]
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// The time the cluster was last modified in [`DateTime<Utc>`].
|
||||
#[serde(default)]
|
||||
pub modified_at: DateTime<Utc>,
|
||||
/// The last time the cluster was played in [`DateTime<Utc>`].
|
||||
/// (Defaults to None if the cluster has never been played)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub played_at: Option<DateTime<Utc>>,
|
||||
/// The overall time played in seconds stored as a [`u64`].
|
||||
#[serde(default)]
|
||||
pub overall_played: u64,
|
||||
/// The recent time played stored as a [`u64`].
|
||||
#[serde(default)]
|
||||
pub recently_played: u64,
|
||||
|
||||
/// The associated [`PackageData`] and modpack data for the cluster.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub package_data: Option<PackageData>,
|
||||
|
||||
/// The user-facing cluster icon as a [`PathBuf`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<PathBuf>,
|
||||
/// The user-facing cluster icon as a URL string.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Optional data used to link a specific cluster to a package project.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct PackageData {
|
||||
/// The package ID as a String.
|
||||
pub package_id: Option<String>,
|
||||
/// The version of the package as a String.
|
||||
pub version_id: Option<String>,
|
||||
/// Whether or not the current package is locked (for legacy modpack support).
|
||||
#[serde(default = "default_locked")]
|
||||
pub locked: Option<bool>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn default_locked() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
/// Available mod loaders to be used for a cluster.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Loader {
|
||||
/// The default vanilla loader, no modding supported.
|
||||
#[default]
|
||||
Vanilla,
|
||||
/// The `MinecraftForge` Minecraft mod loader.
|
||||
Forge,
|
||||
/// The `FabircMC` Minecraft mod loader.
|
||||
Fabric,
|
||||
/// The `NeoForge` Minecraft mod loader.
|
||||
NeoForge,
|
||||
/// The Quilt Minecraft mod loader.
|
||||
Quilt,
|
||||
/// The Legacy Fabric port mod loader.
|
||||
LegacyFabric,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
Self::Vanilla => "vanilla",
|
||||
Self::Forge => "forge",
|
||||
Self::Fabric => "fabric",
|
||||
Self::Quilt => "quilt",
|
||||
Self::NeoForge => "neoforge",
|
||||
Self::LegacyFabric => "legacyfabric",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_meta_str(&self) -> &'static str {
|
||||
match *self {
|
||||
Self::Vanilla => "vanilla",
|
||||
Self::Forge => "forge",
|
||||
Self::Fabric => "fabric",
|
||||
Self::Quilt => "quilt",
|
||||
Self::NeoForge => "neo",
|
||||
Self::LegacyFabric => "legacyfabric",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_string(val: String) -> Self {
|
||||
Self::from_str(val.as_str())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_str(val: &str) -> Self {
|
||||
match val {
|
||||
"forge" => Self::Forge,
|
||||
"fabric" => Self::Fabric,
|
||||
"quilt" => Self::Quilt,
|
||||
"neoforge" => Self::NeoForge,
|
||||
_ => Self::Vanilla,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn supports_mods(&self) -> bool {
|
||||
!matches!(self, Self::Vanilla)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Loader {
|
||||
type Error = crate::ErrorKind;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Ok(match value.to_lowercase().as_str() {
|
||||
"vanilla" => Self::Vanilla,
|
||||
"forge" => Self::Forge,
|
||||
"fabric" => Self::Fabric,
|
||||
"neoforge" => Self::NeoForge,
|
||||
"quilt" => Self::Quilt,
|
||||
"legacyfabric" | "legacy_fabric" => Self::LegacyFabric,
|
||||
_ => return Err(anyhow::anyhow!("invalid loader type").into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Loader {
|
||||
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::NeoForge => "NeoForge",
|
||||
Self::Quilt => "Quilt",
|
||||
Self::LegacyFabric => "LegacyFabric",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom Java arguments on a per-cluster basis.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct JavaOptions {
|
||||
/// A custom java version from the global java store, if specified.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_version: Option<JavaVersion>,
|
||||
/// Custom runtime arguments when running the cluster.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_arguments: Option<Vec<String>>,
|
||||
/// Custom environment variables when running the cluster.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_env_arguments: Option<Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl Cluster {
|
||||
/// Create a new Cluster
|
||||
#[tracing::instrument]
|
||||
pub async fn new(uuid: Uuid, name: String, version: String) -> crate::Result<Self> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!("invalid instance name (empty)").into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
uuid,
|
||||
stage: ClusterStage::NotInstalled,
|
||||
path: PathBuf::new().join(&name),
|
||||
meta: ClusterMeta {
|
||||
name,
|
||||
group: None,
|
||||
mc_version: version,
|
||||
loader: Loader::Vanilla,
|
||||
loader_version: None,
|
||||
created_at: Utc::now(),
|
||||
modified_at: Utc::now(),
|
||||
package_data: None,
|
||||
played_at: None,
|
||||
overall_played: 0,
|
||||
recently_played: 0,
|
||||
icon: None,
|
||||
icon_url: None,
|
||||
},
|
||||
java: None,
|
||||
memory: None,
|
||||
resolution: None,
|
||||
force_fullscreen: None,
|
||||
init_hooks: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the [`ClusterPath`] of the specified [`Cluster`].
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn cluster_path(&self) -> ClusterPath {
|
||||
ClusterPath::new(&self.path)
|
||||
}
|
||||
|
||||
/// Set the icon [`bytes::Bytes`] for this cluster.
|
||||
#[tracing::instrument(skip(self, io_semaphore, icon))]
|
||||
pub async fn set_icon_path<'a>(
|
||||
&'a mut self,
|
||||
cache_path: &Path,
|
||||
io_semaphore: &IoSemaphore,
|
||||
icon: &Path,
|
||||
file_name: &str,
|
||||
) -> crate::Result<()> {
|
||||
let icon = io::read(icon).await?;
|
||||
self.set_icon_bytes(cache_path, io_semaphore, icon.into(), file_name)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the icon [`bytes::Bytes`] for this cluster.
|
||||
#[tracing::instrument(skip(self, io_semaphore, icon))]
|
||||
pub async fn set_icon_bytes<'a>(
|
||||
&'a mut self,
|
||||
cache_path: &Path,
|
||||
io_semaphore: &IoSemaphore,
|
||||
icon: bytes::Bytes,
|
||||
file_name: &str,
|
||||
) -> crate::Result<()> {
|
||||
let file = write_icon(file_name, cache_path, icon, io_semaphore).await?;
|
||||
self.meta.icon = Some(file);
|
||||
self.meta.modified_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a cluster crash.
|
||||
pub fn handle_crash(path: ClusterPath) {
|
||||
tokio::task::spawn(async move {
|
||||
let res = async {
|
||||
let cluster = crate::api::cluster::get(&path).await?;
|
||||
if let Some(cluster) = cluster {
|
||||
if cluster.stage == ClusterStage::Installed {
|
||||
send_message(&format!(
|
||||
"cluster {} has crashed! visit the logs page for a crash report.",
|
||||
cluster.meta.name
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => tracing::warn!("failed to send crash report to frontend {err}"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// get the full path to the current cluster.
|
||||
pub async fn get_full_path(&self) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
let clusters_dir = state.directories.clusters_dir().await;
|
||||
Ok(clusters_dir.join(&self.path))
|
||||
}
|
||||
|
||||
/// get full paths and subpaths
|
||||
pub async fn get_full_subs(&self) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut files = Vec::new();
|
||||
let cluster_path = self.get_full_path().await?;
|
||||
let mut paths = |path: &str| {
|
||||
let new = cluster_path.join(path);
|
||||
if new.exists() {
|
||||
for sub in std::fs::read_dir(&new).map_err(|e| IOError::with_path(e, &new))? {
|
||||
let sub = sub.map_err(IOError::from)?.path();
|
||||
if sub.is_file() {
|
||||
files.push(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
};
|
||||
|
||||
paths(PackageType::Mod.get_folder())?;
|
||||
paths(PackageType::ShaderPack.get_folder())?;
|
||||
paths(PackageType::ResourcePack.get_folder())?;
|
||||
paths(PackageType::DataPack.get_folder())?;
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// watch the filesystem for changes with [`notify`].
|
||||
#[tracing::instrument(skip(watcher))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn watch(
|
||||
cluster_path: &Path,
|
||||
watcher: &mut Debouncer<RecommendedWatcher>,
|
||||
) -> crate::Result<()> {
|
||||
async fn watch_path(
|
||||
cluster_path: &Path,
|
||||
watcher: &mut Debouncer<RecommendedWatcher>,
|
||||
path: &str,
|
||||
) -> crate::Result<()> {
|
||||
let path = cluster_path.join(path);
|
||||
io::create_dir_all(&path).await?;
|
||||
watcher
|
||||
.watcher()
|
||||
.watch(&cluster_path.join(path), notify::RecursiveMode::Recursive)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
watch_path(cluster_path, watcher, PackageType::Mod.get_folder()).await?;
|
||||
watch_path(cluster_path, watcher, PackageType::ShaderPack.get_folder()).await?;
|
||||
watch_path(
|
||||
cluster_path,
|
||||
watcher,
|
||||
PackageType::ResourcePack.get_folder(),
|
||||
)
|
||||
.await?;
|
||||
watch_path(cluster_path, watcher, PackageType::DataPack.get_folder()).await?;
|
||||
watch_path(cluster_path, watcher, "crash-reports").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clusters {
|
||||
/// Initialize the cluster manager and HashMap.
|
||||
#[tracing::instrument(skip(watcher))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn initialize(
|
||||
dirs: &Directories,
|
||||
watcher: &mut Debouncer<RecommendedWatcher>,
|
||||
) -> crate::Result<Self> {
|
||||
let mut clusters = HashMap::new();
|
||||
let clusters_dir = dirs.clusters_dir().await;
|
||||
io::create_dir_all(&&clusters_dir).await?;
|
||||
watcher
|
||||
.watcher()
|
||||
.watch(&clusters_dir, notify::RecursiveMode::NonRecursive)?;
|
||||
let mut files = io::read_dir(&dirs.clusters_dir().await).await?;
|
||||
|
||||
while let Some(file) = files.next_entry().await.map_err(IOError::from)? {
|
||||
let path = file.path();
|
||||
if path.is_dir() {
|
||||
let cluster = match Self::from_dir(&path, dirs).await {
|
||||
Ok(cluster) => Some(cluster),
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to load cluster {err}. skipping");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(cluster_) = cluster {
|
||||
let path = io::canonicalize(path)?;
|
||||
Cluster::watch(&path, watcher).await?;
|
||||
clusters.insert(cluster_.cluster_path(), cluster_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(clusters))
|
||||
}
|
||||
|
||||
/// update registered packages
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn update_packages() {
|
||||
// TODO: Pacakges
|
||||
}
|
||||
|
||||
/// update all available package versions
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn update_versions() {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
let mut updateable: Vec<(ClusterPath, String)> = Vec::new();
|
||||
|
||||
{
|
||||
let clusters = state.clusters.read().await;
|
||||
for (cluster_path, cluster) in &clusters.0 {
|
||||
if let Some(package_data) = &cluster.meta.package_data {
|
||||
if let Some(linked_package) = &package_data.package_id {
|
||||
updateable.push((cluster_path.clone(), linked_package.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
{
|
||||
let clusters = state.clusters.read().await;
|
||||
clusters.sync().await?;
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => tracing::warn!("failed to update managed packages: {err}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// insert a cluster into the HashMap
|
||||
#[tracing::instrument(skip(self, cluster))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn insert(&mut self, cluster: Cluster, dont_watch: bool) -> crate::Result<&Self> {
|
||||
send_cluster(
|
||||
cluster.uuid,
|
||||
&cluster.cluster_path(),
|
||||
&cluster.meta.name,
|
||||
ClusterPayloadType::Inserted,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !dont_watch {
|
||||
let state = State::get().await?;
|
||||
let mut watcher = state.watcher.write().await;
|
||||
Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?;
|
||||
}
|
||||
|
||||
let cluster_name = cluster.cluster_path();
|
||||
cluster_name.validate()?;
|
||||
self.0.insert(cluster_name, cluster);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// remove a cluster from the HashMap
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn remove(&mut self, cluster_path: &ClusterPath) -> crate::Result<Option<Cluster>> {
|
||||
let cluster = self.0.remove(cluster_path);
|
||||
let path = cluster_path.full_path().await?;
|
||||
if path.exists() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
|
||||
Ok(cluster)
|
||||
}
|
||||
|
||||
/// sync all available clusters
|
||||
#[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, |(_, cluster)| async move {
|
||||
let json = serde_json::to_vec(&cluster)?;
|
||||
let json_path = cluster.get_full_path().await?.join(CLUSTER_FILE);
|
||||
io::write(&json_path, &json).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// read a cluster from a directory
|
||||
async fn from_dir(path: &Path, dirs: &Directories) -> crate::Result<Cluster> {
|
||||
let json = io::read(&path.join(CLUSTER_FILE)).await?;
|
||||
let mut cluster = serde_json::from_slice::<Cluster>(&json)?;
|
||||
cluster.path = PathBuf::from(
|
||||
path.strip_prefix(dirs.clusters_dir().await)
|
||||
.map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?,
|
||||
);
|
||||
Ok(cluster)
|
||||
}
|
||||
|
||||
/// sync a cluster
|
||||
pub fn sync_cluster(cluster_path: ClusterPath) {
|
||||
tokio::task::spawn(async move {
|
||||
let span = tracing::span!(tracing::Level::INFO, "sync_cluster");
|
||||
let res = span.in_scope(|| async {
|
||||
let state = State::get().await?;
|
||||
let dirs = &state.directories;
|
||||
let mut clusters = state.clusters.write().await;
|
||||
|
||||
if let Some(cluster) = clusters.0.get_mut(&cluster_path) {
|
||||
if !cluster.get_full_path().await?.exists() {
|
||||
send_cluster(
|
||||
cluster.uuid,
|
||||
&cluster_path,
|
||||
&cluster.meta.name,
|
||||
ClusterPayloadType::Deleted,
|
||||
)
|
||||
.await?;
|
||||
tracing::debug!("removed non-existant fs cluster!");
|
||||
clusters.0.remove(&cluster_path);
|
||||
}
|
||||
} else if cluster_path.full_path().await?.exists() {
|
||||
clusters
|
||||
.insert(
|
||||
Self::from_dir(&cluster_path.full_path().await?, dirs).await?,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
// Cluster::sync_packages(&cluster_path).await;
|
||||
});
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => tracing::warn!("failed to fetch a cluster: {err}"),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
//! Core credential management and storage with IOTA Stronghold.
|
||||
|
||||
use super::Directories;
|
||||
use crate::utils::http::IoSemaphore;
|
||||
use iota_stronghold::{KeyProvider, SnapshotPath, Stronghold};
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// A K/V encrypted store to handle digital secrets using IOTA Stronghold.
|
||||
#[derive(Debug)]
|
||||
pub struct Credentials {
|
||||
inner: Stronghold,
|
||||
path: SnapshotPath,
|
||||
keyprovider: KeyProvider,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Initialize the K/V encrypted stronghold.
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
path: P,
|
||||
password: Zeroizing<Vec<u8>>,
|
||||
_dirs: &Directories,
|
||||
_io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let path = SnapshotPath::from_path(path);
|
||||
let stronghold = Stronghold::default();
|
||||
let keyprovider = KeyProvider::try_from(password).map_err(StrongholdError::MemoryError)?;
|
||||
if path.exists() {
|
||||
stronghold
|
||||
.load_snapshot(&keyprovider, &path)
|
||||
.map_err(StrongholdError::ClientError)?;
|
||||
}
|
||||
Ok(Self {
|
||||
inner: stronghold,
|
||||
path,
|
||||
keyprovider,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save(&self) -> crate::Result<()> {
|
||||
self.inner
|
||||
.commit_with_keyprovider(&self.path, &self.keyprovider)
|
||||
.map_err(StrongholdError::ClientError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn inner(&self) -> &Stronghold {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Credentials {
|
||||
type Target = Stronghold;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum StrongholdError {
|
||||
#[error("stronghold not initialized")]
|
||||
StrongholdNotInitialized,
|
||||
#[error(transparent)]
|
||||
ClientError(#[from] iota_stronghold::ClientError),
|
||||
#[error(transparent)]
|
||||
MemoryError(#[from] iota_stronghold::MemoryError),
|
||||
#[error(transparent)]
|
||||
ProcedureError(#[from] iota_stronghold::procedures::ProcedureError),
|
||||
}
|
||||
|
||||
impl Serialize for StrongholdError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
//! Handles core directory management
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::Settings;
|
||||
use crate::constants;
|
||||
|
||||
/// Directory management and utilities.
|
||||
#[derive(Debug)]
|
||||
pub struct Directories {
|
||||
/// Settings: All small caches and the core settings.json
|
||||
pub settings_dir: PathBuf,
|
||||
/// Base Configuration: Clusters and all Assets (should be changable as a setting that can go across file systems)
|
||||
/// By default this should be the same as the `settings_dir`
|
||||
pub config_dir: RwLock<PathBuf>,
|
||||
/// The current working directory of the directory manager.
|
||||
pub cwd: PathBuf,
|
||||
}
|
||||
|
||||
impl Directories {
|
||||
/// Bootstrap the initial settings directory on first startup.
|
||||
#[must_use]
|
||||
pub fn init_settings_dir() -> Option<PathBuf> {
|
||||
Self::env_path("ONELAUNCHER_CONFIG")
|
||||
.or_else(|| Some(dirs::data_dir()?.join(constants::NAME)))
|
||||
}
|
||||
|
||||
/// Bootstrap the initial settings directory on first startup.
|
||||
#[inline]
|
||||
pub fn init_settings_file() -> crate::Result<PathBuf> {
|
||||
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
|
||||
Ok(settings_dir.join("settings.json"))
|
||||
}
|
||||
|
||||
/// Initialize the core directory manager
|
||||
#[tracing::instrument]
|
||||
pub fn initalize(settings: &Settings) -> crate::Result<Self> {
|
||||
let cwd = std::env::current_dir().map_err(DirectoryError::WorkingDir)?;
|
||||
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
|
||||
let config_dir = settings
|
||||
.config_dir
|
||||
.clone()
|
||||
.ok_or(DirectoryError::ConfigDir)?;
|
||||
Ok(Self {
|
||||
settings_dir,
|
||||
config_dir: RwLock::new(config_dir),
|
||||
cwd,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata` folder within the core config directory.
|
||||
#[inline]
|
||||
pub async fn metadata_dir(&self) -> PathBuf {
|
||||
self.config_dir
|
||||
.read()
|
||||
.await
|
||||
.join(constants::METADATA_FOLDER)
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/libraries` folder for Minecraft libraries.
|
||||
#[inline]
|
||||
pub async fn libraries_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("libraries")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/java` directory.
|
||||
#[inline]
|
||||
pub async fn java_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("java_versions")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/caches` directory.
|
||||
#[inline]
|
||||
pub async fn caches_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("caches")
|
||||
}
|
||||
|
||||
/// Bootstrap the core logs directory and get it as a [`PathBuf`].
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn logs_dir() -> Option<PathBuf> {
|
||||
Self::init_settings_dir().map(|d| d.join("logs"))
|
||||
}
|
||||
|
||||
/// Get the `settings_dir/settings.json` file as a [`PathBuf`].
|
||||
#[inline]
|
||||
pub fn settings_file(&self) -> PathBuf {
|
||||
self.settings_dir.join(constants::SETTINGS_FILE)
|
||||
}
|
||||
|
||||
/// Get a [`PathBuf`] from a provided environment variable.
|
||||
#[inline]
|
||||
fn env_path(name: &str) -> Option<PathBuf> {
|
||||
std::env::var_os(name).map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/assets` directory.
|
||||
#[inline]
|
||||
pub async fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("assets")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/assets/indexes` directory.
|
||||
#[inline]
|
||||
pub async fn index_dir(&self) -> PathBuf {
|
||||
self.assets_dir().await.join("indexes")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/assets/objects` directory.
|
||||
#[inline]
|
||||
pub async fn objects_dir(&self) -> PathBuf {
|
||||
self.assets_dir().await.join("objects")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/assets/objects/{hash}` directory.
|
||||
#[inline]
|
||||
pub async fn object_dir(&self, hash: &str) -> PathBuf {
|
||||
self.objects_dir().await.join(&hash[..2]).join(hash)
|
||||
}
|
||||
|
||||
/// Get the `config_dir/clusters` directory.
|
||||
#[inline]
|
||||
pub async fn clusters_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("clusters")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/clusters/{uuid}` directory.
|
||||
#[inline]
|
||||
pub async fn cluster_dir(&self, uuid: uuid::Uuid) -> PathBuf {
|
||||
self.clusters_dir().await.join(uuid.to_string())
|
||||
}
|
||||
|
||||
/// Get the `{cluster_path}/logs` directory.
|
||||
#[inline]
|
||||
pub async fn cluster_logs_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
|
||||
Ok(cluster_path.full_path().await?.join("logs"))
|
||||
}
|
||||
|
||||
/// Get the `{cluster_path}/crash_reports` directory.
|
||||
#[inline]
|
||||
pub async fn crash_reports_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
|
||||
Ok(cluster_path.full_path().await?.join("crash-reports"))
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/resources` directory.
|
||||
#[inline]
|
||||
pub async fn legacy_assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("resources")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/natives` directory.
|
||||
#[inline]
|
||||
pub async fn natives_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("natives")
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/natives/{version}` directory.
|
||||
#[inline]
|
||||
pub async fn version_natives_dir(&self, version: &str) -> PathBuf {
|
||||
self.natives_dir().await.join(version)
|
||||
}
|
||||
|
||||
/// Get the Minecraft `config_dir/metadata/versions` directory.
|
||||
#[inline]
|
||||
pub async fn versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("versions")
|
||||
}
|
||||
|
||||
/// Get the Minecraft `config_dir/metadata/versions/{version}` directory.
|
||||
#[inline]
|
||||
pub async fn version_dir(&self, version: &str) -> PathBuf {
|
||||
self.versions_dir().await.join(version)
|
||||
}
|
||||
|
||||
/// Get the `config_dir/icons` directory.
|
||||
#[inline]
|
||||
pub async fn icons_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("icons")
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a core directory management error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DirectoryError {
|
||||
/// An error for when a config directory cannot be initialized.
|
||||
#[error("failed to get the user's config directory")]
|
||||
ConfigDir,
|
||||
/// A wrapper over [`std::io::Error`] for cwd fetching errors.
|
||||
#[error("could not open working directory {0}")]
|
||||
WorkingDir(std::io::Error),
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
//! Handles Discord rich prescence integration
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use discord_rich_presence::activity::{Activity, Assets, Button};
|
||||
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::constants::DISCORD_RPC_CLIENT_ID;
|
||||
use crate::State;
|
||||
|
||||
/// A structure used to manage a Discord IPC client.
|
||||
pub struct DiscordRPC {
|
||||
/// The managed [`DiscordIpcClient`] used for this IPC client.
|
||||
client: Arc<RwLock<DiscordIpcClient>>,
|
||||
/// Whether or not we are connected to the IPC server and internet.
|
||||
connected: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl DiscordRPC {
|
||||
/// Initialize the Discord IPC client and attempt to connect to the server.
|
||||
/// If an error is returned, it will be ignored since Discord RPC isnt essential for the launcher to load.
|
||||
pub fn initialize(is_offline: bool) -> crate::Result<Self> {
|
||||
let mut discord_ipc = DiscordIpcClient::new(DISCORD_RPC_CLIENT_ID)
|
||||
.map_err(|e| anyhow::anyhow!("failed to create discord client {}", e))?;
|
||||
|
||||
let connected = if is_offline {
|
||||
Arc::new(AtomicBool::new(false))
|
||||
} else {
|
||||
let result = discord_ipc.connect();
|
||||
if result.is_ok() {
|
||||
Arc::new(AtomicBool::new(true))
|
||||
} else {
|
||||
Arc::new(AtomicBool::new(false))
|
||||
}
|
||||
};
|
||||
|
||||
let client = Arc::new(RwLock::new(discord_ipc));
|
||||
Ok(Self { client, connected })
|
||||
}
|
||||
|
||||
/// Set the Discord IPC activity with a message and try to reconnect if the initial connection fails.
|
||||
pub async fn set_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> {
|
||||
if !self.retry_online().await {
|
||||
return Ok(());
|
||||
}
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
if settings.disable_discord {
|
||||
Ok(self.clear_activity(true).await?)
|
||||
} else {
|
||||
Ok(self.apply_activity(msg, reconnect_if_fail).await?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not use this function, use [`DiscordRPC#set_activity`]
|
||||
pub async fn apply_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> {
|
||||
if !self.retry_connection().await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let activity = Activity::new()
|
||||
.state(msg)
|
||||
.buttons(vec![Button::new("Download", "https://polyfrost.org/")])
|
||||
.assets(
|
||||
Assets::new()
|
||||
.large_image("onelauncher_logo_512")
|
||||
.large_text("OneLauncher"),
|
||||
);
|
||||
|
||||
let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> =
|
||||
self.client.write().await;
|
||||
let result = client.set_activity(activity.clone());
|
||||
let could_not_set = |e: Box<dyn serde::ser::StdError>| {
|
||||
anyhow::anyhow!("failed to apply discord activity {}", e)
|
||||
};
|
||||
|
||||
if reconnect_if_fail {
|
||||
if let Err(_e) = result {
|
||||
client
|
||||
.reconnect()
|
||||
.map_err(|e| anyhow::anyhow!("failed to reconnect to Discord RPC {}", e))?;
|
||||
|
||||
return Ok(client.set_activity(activity).map_err(could_not_set)?);
|
||||
}
|
||||
} else {
|
||||
result.map_err(could_not_set)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to check if we are still in an offline environment.
|
||||
pub async fn retry_online(&self) -> bool {
|
||||
let Ok(state) = State::get().await else {
|
||||
return false;
|
||||
};
|
||||
let offline = state.offline.read().await;
|
||||
if *offline {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Retry the Discord IPC connection.
|
||||
pub async fn retry_connection(&self) -> bool {
|
||||
let mut client = self.client.write().await;
|
||||
if !self.connected.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
if client.connect().is_ok() {
|
||||
self.connected
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Clear the current Discord IPC activity, reconnecting if requested.
|
||||
pub async fn clear_activity(&self, reconnect_if_fail: bool) -> crate::Result<()> {
|
||||
if !self.retry_online().await || !self.retry_connection().await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut client = self.client.write().await;
|
||||
let result = client.clear_activity();
|
||||
let could_not_clear = |e: Box<dyn serde::ser::StdError>| {
|
||||
anyhow::anyhow!("failed to clear discord activity {}", e)
|
||||
};
|
||||
|
||||
if reconnect_if_fail {
|
||||
if result.is_err() {
|
||||
client
|
||||
.reconnect()
|
||||
.map_err(|e| anyhow::anyhow!("failed to reconnect to Discord RPC {}", e))?;
|
||||
|
||||
return Ok(client.clear_activity().map_err(could_not_clear)?);
|
||||
}
|
||||
} else {
|
||||
result.map_err(could_not_clear)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear and disable the Discord IPC, reconnecting if requested.
|
||||
pub async fn clear(&self, reconnect_if_fail: bool) -> crate::Result<()> {
|
||||
let state: Arc<tokio::sync::RwLockReadGuard<'_, State>> = State::get().await?;
|
||||
|
||||
{
|
||||
let settings = state.settings.read().await;
|
||||
if settings.disable_discord {
|
||||
println!("discord is disabled.. clearing");
|
||||
return self.clear_activity(true).await;
|
||||
}
|
||||
}
|
||||
|
||||
self.set_activity("Idling...", reconnect_if_fail).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
//! Handles all available Java installations
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::utils::java;
|
||||
use crate::utils::java::JavaVersion;
|
||||
|
||||
/// A `HashMap` of all located and installed available Java versions
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct JavaVersions(HashMap<String, JavaVersion>);
|
||||
|
||||
impl Default for JavaVersions {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl JavaVersions {
|
||||
/// Create an empty `JavaVersions` `HashMap`.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
/// Inserts a key-value pair into the map.
|
||||
pub fn insert(&mut self, key: String, java: JavaVersion) {
|
||||
self.0.insert(key, java);
|
||||
}
|
||||
|
||||
/// Removes a key from the map, returning the value at the key if the key was previously in the map.
|
||||
pub fn remove(&mut self, key: &String) {
|
||||
self.0.remove(key);
|
||||
}
|
||||
|
||||
/// Returns a reference to the value corresponding to the key.
|
||||
#[must_use]
|
||||
pub fn get(&self, key: &String) -> Option<&JavaVersion> {
|
||||
self.0.get(key)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the value corresponding to the key.
|
||||
pub fn get_mut(&mut self, key: &String) -> Option<&mut JavaVersion> {
|
||||
self.0.get_mut(key)
|
||||
}
|
||||
|
||||
/// Returns the number of elements in the map.
|
||||
#[must_use]
|
||||
pub fn count(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// A collection visiting all keys in arbitrary order.
|
||||
#[must_use]
|
||||
pub fn keys(&self) -> Vec<String> {
|
||||
self.0.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Validates all stored java versions.
|
||||
pub async fn validate(&self) -> bool {
|
||||
for java in self.0.values() {
|
||||
let runtime = java::check_java_instance(PathBuf::from(&java.path).as_path()).await;
|
||||
if let Some(runtime) = runtime {
|
||||
if runtime.version != java.version {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
//! Handles fetching metadata via interpulse.
|
||||
|
||||
use interpulse::api::minecraft::VersionManifest as MinecraftManifest;
|
||||
use interpulse::api::modded::Manifest as ModdedManifest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::constants::METADATA_API_URL;
|
||||
use crate::utils::http::{fetch, read_json, write, FetchSemaphore, IoSemaphore};
|
||||
use crate::State;
|
||||
|
||||
use super::Directories;
|
||||
|
||||
/// A structure of manifests and metadata fetching utilities.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Metadata {
|
||||
/// The [`MinecraftManifest`] associated with core Minecraft versions.
|
||||
pub minecraft: Option<MinecraftManifest>,
|
||||
/// The [`ModdedManifest`] associated with core Fabric versions.
|
||||
pub fabric: Option<ModdedManifest>,
|
||||
/// The [`ModdedManifest`] associated with core Quilt versions.
|
||||
pub quilt: Option<ModdedManifest>,
|
||||
/// The [`ModdedManifest`] associated with core `NeoForge` versions.
|
||||
pub neoforge: Option<ModdedManifest>,
|
||||
/// The [`ModdedManifest`] associated with core Forge versions.
|
||||
pub forge: Option<ModdedManifest>,
|
||||
// /// The [`ModdedManifest`] associated with core Legacy Fabric versions.
|
||||
// pub legacy_fabric: Option<ModdedManifest>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Get the formatted manifest for a specific [`Metadata`] type.
|
||||
fn get_manifest(name: &str, version: usize) -> String {
|
||||
format!("{METADATA_API_URL}/{name}/v{version}/manifest.json")
|
||||
}
|
||||
|
||||
/// Fetch all available metadata types that are currently [`None`] and get a new [`Metadata`] structure.
|
||||
/// Useful for when reading from cache and a certain object not existing
|
||||
pub async fn fetch_errored(&mut self, semaphore: &FetchSemaphore) -> crate::Result<bool> {
|
||||
let mut did_change = false;
|
||||
if self.minecraft.is_none() {
|
||||
self.minecraft = Self::fetch_minecraft(semaphore).await;
|
||||
|
||||
if self.minecraft.is_some() {
|
||||
did_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.fabric.is_none() {
|
||||
self.fabric = Self::fetch_fabric(semaphore).await;
|
||||
|
||||
if self.fabric.is_some() {
|
||||
did_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.quilt.is_none() {
|
||||
self.quilt = Self::fetch_quilt(semaphore).await;
|
||||
|
||||
if self.quilt.is_some() {
|
||||
did_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.neoforge.is_none() {
|
||||
self.neoforge = Self::fetch_neoforge(semaphore).await;
|
||||
|
||||
if self.neoforge.is_some() {
|
||||
did_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.forge.is_none() {
|
||||
self.forge = Self::fetch_forge(semaphore).await;
|
||||
|
||||
if self.forge.is_some() {
|
||||
did_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(did_change)
|
||||
}
|
||||
|
||||
/// Fetch all available metadata types and get a new [`Metadata`] structure.
|
||||
pub async fn fetch(semaphore: &FetchSemaphore) -> crate::Result<Self> {
|
||||
let (minecraft, fabric, quilt, neoforge, forge) = tokio::join! {
|
||||
Self::fetch_minecraft(semaphore),
|
||||
Self::fetch_fabric(semaphore),
|
||||
Self::fetch_quilt(semaphore),
|
||||
Self::fetch_neoforge(semaphore),
|
||||
Self::fetch_forge(semaphore)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
minecraft,
|
||||
fabric,
|
||||
quilt,
|
||||
neoforge,
|
||||
forge,
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_minecraft(semaphore: &FetchSemaphore) -> Option<MinecraftManifest> {
|
||||
let url = Self::get_manifest(
|
||||
"minecraft",
|
||||
interpulse::api::minecraft::CURRENT_FORMAT_VERSION,
|
||||
);
|
||||
fetch_version_manifest(Some(&url), semaphore).await.ok()
|
||||
}
|
||||
|
||||
async fn fetch_fabric(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
|
||||
let url = Self::get_manifest(
|
||||
"fabric",
|
||||
interpulse::api::modded::CURRENT_FABRIC_FORMAT_VERSION,
|
||||
);
|
||||
fetch_modded_manifest(&url, semaphore).await.ok()
|
||||
}
|
||||
|
||||
async fn fetch_quilt(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
|
||||
let url = Self::get_manifest(
|
||||
"quilt",
|
||||
interpulse::api::modded::CURRENT_QUILT_FORMAT_VERSION,
|
||||
);
|
||||
fetch_modded_manifest(&url, semaphore).await.ok()
|
||||
}
|
||||
|
||||
async fn fetch_neoforge(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
|
||||
let url = Self::get_manifest(
|
||||
"neoforge",
|
||||
interpulse::api::modded::CURRENT_NEOFORGE_FORMAT_VERSION,
|
||||
);
|
||||
fetch_modded_manifest(&url, semaphore).await.ok()
|
||||
}
|
||||
|
||||
async fn fetch_forge(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
|
||||
let url = Self::get_manifest(
|
||||
"forge",
|
||||
interpulse::api::modded::CURRENT_FORGE_FORMAT_VERSION,
|
||||
);
|
||||
fetch_modded_manifest(&url, semaphore).await.ok()
|
||||
}
|
||||
|
||||
/// Initialize the core Metadata manager.
|
||||
#[tracing::instrument(skip(io_semaphore, fetch_semaphore))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn initialize(
|
||||
dirs: &Directories,
|
||||
online: bool,
|
||||
io_semaphore: &IoSemaphore,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
let path = dirs.caches_dir().await.join("metadata.json");
|
||||
let backup = dirs.caches_dir().await.join("metadata.json.bak");
|
||||
let mut should_write = false;
|
||||
|
||||
if let Ok(mut metadata_json) = read_json::<Self>(&path, io_semaphore).await {
|
||||
should_write = metadata_json.fetch_errored(fetch_semaphore).await?;
|
||||
|
||||
metadata = Some(metadata_json);
|
||||
} else if online {
|
||||
let res = async {
|
||||
let fetch_data = Self::fetch(fetch_semaphore).await?;
|
||||
should_write = true;
|
||||
|
||||
metadata = Some(fetch_data);
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to fetch metadata: {err}");
|
||||
}
|
||||
}
|
||||
} else if let Ok(metadata_json) = read_json::<Self>(&backup, io_semaphore).await {
|
||||
metadata = Some(metadata_json);
|
||||
onelauncher_utils::io::copy(&backup, &path).await?;
|
||||
}
|
||||
|
||||
if should_write {
|
||||
write(
|
||||
&path,
|
||||
&serde_json::to_vec(&metadata).unwrap_or_default(),
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
write(
|
||||
&backup,
|
||||
&serde_json::to_vec(&metadata).unwrap_or_default(),
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
metadata.map_or_else(
|
||||
|| Err(anyhow::anyhow!("failed to fetch launcher metadata").into()),
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
/// Update and backup all available metadata.
|
||||
pub async fn update() {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
let fetch_data = Self::fetch(&state.fetch_semaphore).await?;
|
||||
|
||||
let path = state.directories.caches_dir().await.join("metadata.json");
|
||||
let backup = state
|
||||
.directories
|
||||
.caches_dir()
|
||||
.await
|
||||
.join("metadata.json.bak");
|
||||
|
||||
if path.exists() {
|
||||
onelauncher_utils::io::copy(&path, &backup).await?;
|
||||
}
|
||||
|
||||
write(
|
||||
&path,
|
||||
&serde_json::to_vec(&fetch_data)?,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut old_metadata = state.metadata.write().await;
|
||||
*old_metadata = fetch_data;
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to update launcher metadata: {err}");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_version_manifest(
|
||||
url: Option<&str>,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<MinecraftManifest> {
|
||||
let url = url.unwrap_or(interpulse::api::minecraft::VERSION_MANIFEST_URL);
|
||||
Ok(serde_json::from_slice(
|
||||
&fetch(url, None, semaphore).await.inspect_err(|_| {
|
||||
tracing::error!("couldn't fetch version manifest at '{}'", url);
|
||||
})?,
|
||||
)?)
|
||||
}
|
||||
|
||||
async fn fetch_modded_manifest(
|
||||
url: &str,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<ModdedManifest> {
|
||||
Ok(serde_json::from_slice(
|
||||
&fetch(url, None, semaphore).await.inspect_err(|_| {
|
||||
tracing::error!("couldn't fetch modded manifest at '{}'", url);
|
||||
})?,
|
||||
)?)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,289 +0,0 @@
|
|||
//! **`OneLauncher` Store**
|
||||
//!
|
||||
//! Core state and storage management for the launcher, managing all states.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::RecommendedWatcher;
|
||||
use notify_debouncer_mini::Debouncer;
|
||||
use tokio::sync::{OnceCell, RwLock, Semaphore};
|
||||
|
||||
use crate::proxy::send::{init_ingress_internal, send_ingress, send_offline};
|
||||
use crate::utils::http;
|
||||
use crate::utils::http::{FetchSemaphore, IoSemaphore};
|
||||
|
||||
mod dirs;
|
||||
pub use self::dirs::*;
|
||||
|
||||
mod minecraft;
|
||||
pub use self::minecraft::*;
|
||||
|
||||
mod credentials;
|
||||
pub use self::credentials::*;
|
||||
|
||||
mod discord;
|
||||
pub use self::discord::*;
|
||||
|
||||
mod metadata;
|
||||
pub use self::metadata::*;
|
||||
|
||||
mod settings;
|
||||
pub use self::settings::*;
|
||||
|
||||
mod java_versions;
|
||||
pub use self::java_versions::*;
|
||||
|
||||
mod processor;
|
||||
pub use self::processor::*;
|
||||
|
||||
mod clusters;
|
||||
pub use self::clusters::*;
|
||||
|
||||
mod package;
|
||||
pub use self::package::*;
|
||||
|
||||
mod oneconfig;
|
||||
pub use self::oneconfig::*;
|
||||
|
||||
/// The static [`OnceCell<RwLock<State>>`] for storing the global runtime launcher state.
|
||||
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
|
||||
|
||||
/// The public `OneLauncher` state structure.
|
||||
pub struct State {
|
||||
/// Handles a boolean to check if the launcher is connected to the internet
|
||||
pub offline: RwLock<bool>,
|
||||
/// Handles core launcher directory and files
|
||||
pub directories: Directories,
|
||||
|
||||
/// Semaphore used to handle concurrent network requests
|
||||
pub fetch_semaphore: FetchSemaphore,
|
||||
/// Maximum number of semaphore permits to limit the amount of network requests at one time
|
||||
pub fetch_semaphore_max: RwLock<u32>,
|
||||
/// Semaphore used to handle concurrent IO requests
|
||||
pub io_semaphore: IoSemaphore,
|
||||
/// Maximum number of semaphore permits to limit the amount of IO requests at one time
|
||||
pub io_semaphore_max: RwLock<u32>,
|
||||
|
||||
/// Handles launcher metadata via [`interpulse`]
|
||||
pub metadata: RwLock<Metadata>,
|
||||
/// Handles launcher configuration and settings
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Handles minecraft auth flows and users
|
||||
pub users: RwLock<MinecraftState>,
|
||||
/// Handles processes
|
||||
pub processor: RwLock<Processor>,
|
||||
/// Handles `OneConfig` integration with [`async-tungstenite`]
|
||||
pub oneconfig: RwLock<Option<OneConfig>>,
|
||||
/// Handles secure credential management with [`iota_stronghold`].
|
||||
pub credentials: RwLock<Option<Credentials>>,
|
||||
/// Handles clusters
|
||||
pub(crate) clusters: RwLock<Clusters>,
|
||||
/// Handles packages
|
||||
pub(crate) packages: RwLock<Packages>,
|
||||
/// Handles internal ingress processes
|
||||
pub(crate) ingress_processor: RwLock<IngressProcessor>,
|
||||
/// Handles file system watching for cluster manager
|
||||
pub(crate) watcher: RwLock<Debouncer<RecommendedWatcher>>,
|
||||
/// Handles Discord rich prescence
|
||||
pub discord_rpc: DiscordRPC,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Get the current global launcher state (or initialize it)
|
||||
pub async fn get() -> crate::Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
|
||||
Ok(Arc::new(
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize)
|
||||
.await?
|
||||
.read()
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
/// Get and lock writing to the current global launcher state (or initalize it)
|
||||
pub async fn get_and_write() -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
|
||||
Ok(LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize)
|
||||
.await?
|
||||
.write()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Initializes the core OneLauncher state fully.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
async fn initialize() -> crate::Result<RwLock<Self>> {
|
||||
let ingress =
|
||||
init_ingress_internal(crate::IngressType::Initialize, 100.0, "initializing state")
|
||||
.await?;
|
||||
|
||||
let settings = Settings::initialize(&Directories::init_settings_file()?).await?;
|
||||
let directories = Directories::initalize(&settings)?;
|
||||
send_ingress(&ingress, 10.0, None).await?;
|
||||
let mut watcher = crate::utils::watcher::initialize_watcher()?;
|
||||
let fetch_semaphore =
|
||||
FetchSemaphore(RwLock::new(Semaphore::new(settings.max_async_fetches)));
|
||||
let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new(
|
||||
settings.max_async_io_operations,
|
||||
)));
|
||||
send_ingress(&ingress, 10.0, None).await?;
|
||||
|
||||
let is_offline = !http::check_internet_connection(3).await;
|
||||
|
||||
// TODO: Make this run in the background, this delays launcher startup by a couple seconds
|
||||
let metadata_in =
|
||||
Metadata::initialize(&directories, !is_offline, &io_semaphore, &fetch_semaphore);
|
||||
let clusters_in = Clusters::initialize(&directories, &mut watcher);
|
||||
let users_in = MinecraftState::initialize(&directories, &io_semaphore);
|
||||
let (metadata, clusters, users) = crate::ingress_join! {
|
||||
Some(&ingress), 70.0, Some("loading core");
|
||||
metadata_in,
|
||||
clusters_in,
|
||||
users_in,
|
||||
}?;
|
||||
|
||||
send_ingress(&ingress, 5.0, Some("loading packages")).await?;
|
||||
let packages = Packages::initialize(&clusters);
|
||||
|
||||
let ingress_processor = IngressProcessor::new();
|
||||
let discord_rpc = DiscordRPC::initialize(is_offline || settings.disable_discord)?;
|
||||
if !settings.disable_discord && !is_offline {
|
||||
let _ = discord_rpc.apply_activity("Idling...", true).await;
|
||||
}
|
||||
|
||||
let processor = Processor::new();
|
||||
|
||||
Self::status_loop();
|
||||
send_ingress(&ingress, 10.0, None).await?;
|
||||
|
||||
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
|
||||
offline: RwLock::new(is_offline),
|
||||
directories,
|
||||
fetch_semaphore,
|
||||
fetch_semaphore_max: RwLock::new(settings.max_async_fetches as u32),
|
||||
io_semaphore,
|
||||
io_semaphore_max: RwLock::new(settings.max_async_io_operations as u32),
|
||||
metadata: RwLock::new(metadata),
|
||||
settings: RwLock::new(settings),
|
||||
users: RwLock::new(users),
|
||||
processor: RwLock::new(processor),
|
||||
oneconfig: RwLock::new(None),
|
||||
credentials: RwLock::new(None),
|
||||
clusters: RwLock::new(clusters),
|
||||
packages: RwLock::new(packages),
|
||||
ingress_processor: RwLock::new(ingress_processor),
|
||||
watcher: RwLock::new(watcher),
|
||||
discord_rpc,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Initalizes a "game loop" which checks for updates and if we are online.
|
||||
pub fn status_loop() {
|
||||
tokio::task::spawn(async {
|
||||
loop {
|
||||
let state = Self::get().await;
|
||||
if let Ok(state) = state {
|
||||
let _ = state.check_status().await;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates all data if we are connected to the internet.
|
||||
pub fn update() {
|
||||
tokio::task::spawn(async {
|
||||
let state = Self::get();
|
||||
if let Ok(state) = state.await {
|
||||
if !*state.offline.read().await {
|
||||
let version_up = Clusters::update_versions();
|
||||
let meta_up = Metadata::update();
|
||||
let package_up = Clusters::update_packages();
|
||||
|
||||
let _ = tokio::join!(version_up, meta_up, package_up);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Synchronizes data that can change outside of our control.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
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_clusters = async {
|
||||
let state = Arc::clone(&state);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let clusters = state.clusters.read().await;
|
||||
|
||||
clusters.sync().await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await?
|
||||
};
|
||||
|
||||
tokio::try_join!(sync_settings, sync_clusters)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resets the IO [`Semaphore`] to clear all tasks.
|
||||
/// # This should ONLY be called when we can ensure
|
||||
/// # that there are no current tasks running!!
|
||||
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;
|
||||
// block ALL semaphore permits - this can be destructive
|
||||
let _ = io_semaphore.acquire_many(*total_permits).await;
|
||||
|
||||
io_semaphore.close();
|
||||
*total_permits = settings.max_async_io_operations as u32;
|
||||
*io_semaphore = Semaphore::new(settings.max_async_io_operations);
|
||||
}
|
||||
|
||||
/// Resets the Fetch [`Semaphore`] to clear all tasks.
|
||||
/// # This should ONLY be called when we can ensure
|
||||
/// # that there are no current tasks running!!
|
||||
pub async fn reset_fetch_semaphore(&self) {
|
||||
let settings = self.settings.read().await;
|
||||
let mut fetch_semaphore = self.fetch_semaphore.0.write().await;
|
||||
let mut total_permits = self.fetch_semaphore_max.write().await;
|
||||
// block ALL semaphore permits - this can be destructive
|
||||
let _ = fetch_semaphore.acquire_many(*total_permits).await;
|
||||
|
||||
fetch_semaphore.close();
|
||||
*total_permits = settings.max_async_io_operations as u32;
|
||||
*fetch_semaphore = Semaphore::new(settings.max_async_fetches);
|
||||
}
|
||||
|
||||
/// Checks if we are online used in a loop.
|
||||
pub async fn check_status(&self) -> crate::Result<()> {
|
||||
let is_online = http::check_internet_connection(3).await;
|
||||
let mut offline = self.offline.write().await;
|
||||
if *offline != is_online {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
send_offline(!is_online).await?;
|
||||
*offline = !is_online;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine if the global state is initalized or not (wrapper)
|
||||
pub fn initalized() -> bool {
|
||||
LAUNCHER_STATE.initialized()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
//! `OneConfig` state using [`async_tungstenite`] to communicate to the `OneConfig` server
|
||||
|
||||
use super::ClusterPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Wrapper over an [`async_tungstenite::WebSocketStream`] to connect to the `OneConfig` server.
|
||||
pub struct OneConfig {
|
||||
/// The public tokio socket connection.
|
||||
pub socket: async_tungstenite::WebSocketStream<async_tungstenite::tokio::ConnectStream>,
|
||||
}
|
||||
|
||||
/// A WebSocket packet while updating a mod to display a GUI in game.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ModUpdatePacket {
|
||||
/// The ID of the packet.
|
||||
id: Uuid,
|
||||
/// The ID of the mod being updated.
|
||||
mod_id: Uuid,
|
||||
/// The [`ClusterPath`] associated with this event.
|
||||
cluster_path: ClusterPath,
|
||||
}
|
||||
|
||||
impl OneConfig {
|
||||
/// Initializes a `OneConfig` socket connection on localhost port `4023`.
|
||||
pub async fn initialize(cluster: ClusterPath) -> crate::Result<Self> {
|
||||
let (socket, _) = async_tungstenite::tokio::connect_async(format!(
|
||||
"wss://localhost:4023/oneconfig/ws?cluster={cluster}"
|
||||
))
|
||||
.await?;
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
pub async fn close(&mut self) -> crate::Result<()> {
|
||||
self.socket.close(None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,969 +0,0 @@
|
|||
//! Handlers for Mod metadata that can be displayed in a GUI mod list or exported as a mod pack
|
||||
|
||||
use crate::package::content::Providers;
|
||||
use crate::proxy::send::{init_ingress, send_ingress};
|
||||
use crate::proxy::IngressId;
|
||||
use crate::store::Loader;
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::http::{write_icon, IoSemaphore};
|
||||
use crate::State;
|
||||
use async_zip::tokio::read::fs::ZipFileReader;
|
||||
use chrono::{DateTime, Utc};
|
||||
use onelauncher_utils::io;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::vec;
|
||||
use tokio::fs::DirEntry;
|
||||
|
||||
use super::{ClusterPath, Clusters, Directories, PackagePath};
|
||||
|
||||
/// Represents types of packages handled by the launcher.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PackageType {
|
||||
/// Represents a mod jarfile
|
||||
Mod,
|
||||
/// Represents a datapack file
|
||||
DataPack,
|
||||
/// Represents a resourcepack file
|
||||
ResourcePack,
|
||||
/// Represents a shaderpack file
|
||||
#[serde(alias = "shader")]
|
||||
ShaderPack,
|
||||
/// Represents a modpack file
|
||||
ModPack,
|
||||
}
|
||||
|
||||
impl PackageType {
|
||||
/// attempt to get a [`PackageType`] from a loader string.
|
||||
#[must_use]
|
||||
pub fn from_loader(loaders: &[String]) -> Option<Self> {
|
||||
if loaders
|
||||
.iter()
|
||||
.any(|x| ["fabric", "forge", "quilt"].contains(&&**x))
|
||||
{
|
||||
Some(Self::Mod)
|
||||
} else if loaders.iter().any(|x| x == "datapack") {
|
||||
Some(Self::DataPack)
|
||||
} else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) {
|
||||
Some(Self::ShaderPack)
|
||||
} else if loaders
|
||||
.iter()
|
||||
.any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x))
|
||||
{
|
||||
Some(Self::ResourcePack)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_parent(path: &Path) -> Option<Self> {
|
||||
let path = path.parent()?.file_name()?;
|
||||
match path.to_str()? {
|
||||
"mods" => Some(Self::Mod),
|
||||
"datapacks" => Some(Self::DataPack),
|
||||
"resourcepacks" => Some(Self::ResourcePack),
|
||||
"shaderpacks" => Some(Self::ShaderPack),
|
||||
"modpack" => Some(Self::ModPack),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mod => "mod",
|
||||
Self::DataPack => "datapack",
|
||||
Self::ResourcePack => "resourcepack",
|
||||
Self::ShaderPack => "shader",
|
||||
Self::ModPack => "modpack",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_folder(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mod => "mods",
|
||||
Self::DataPack => "datapacks",
|
||||
Self::ResourcePack => "resourcepacks",
|
||||
Self::ShaderPack => "shaderpacks",
|
||||
Self::ModPack => "modpack",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_meta(&self) -> PathBuf {
|
||||
PathBuf::from(format!(
|
||||
"{}/{}",
|
||||
self.get_folder(),
|
||||
self.get_meta_file_name()
|
||||
))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_meta_file_name(&self) -> String {
|
||||
String::from(".packages.json")
|
||||
}
|
||||
|
||||
pub async fn file_matches(&self, entry: &DirEntry) -> crate::Result<bool> {
|
||||
if !(entry.path().try_exists()?) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(match self {
|
||||
Self::Mod => {
|
||||
entry.file_type().await?.is_file()
|
||||
&& entry.path().extension() == Some("jar".as_ref())
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iterator() -> impl Iterator<Item = Self> {
|
||||
[
|
||||
Self::Mod,
|
||||
Self::DataPack,
|
||||
Self::ResourcePack,
|
||||
Self::ShaderPack,
|
||||
Self::ModPack,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PackageType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct SearchResult {
|
||||
pub slug: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub categories: Vec<String>,
|
||||
pub client_side: PackageSide,
|
||||
pub server_side: PackageSide,
|
||||
pub project_type: PackageType,
|
||||
pub downloads: u64,
|
||||
#[serde(default)]
|
||||
pub icon_url: String,
|
||||
pub project_id: String,
|
||||
pub author: String,
|
||||
pub versions: Vec<String>,
|
||||
pub follows: u32,
|
||||
pub date_created: Option<DateTime<Utc>>,
|
||||
pub date_modified: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct ProviderSearchResults {
|
||||
pub provider: Providers,
|
||||
pub results: Vec<SearchResult>,
|
||||
pub total: u32,
|
||||
}
|
||||
|
||||
/// A struct that represents a Package.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct Package {
|
||||
pub sha1: String,
|
||||
pub meta: PackageMetadata,
|
||||
pub file_name: String,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
pub fn new(path: &PathBuf, meta: PackageMetadata) -> crate::Result<Self> {
|
||||
let file_name = path.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("couldn't get filename of path {path:?}"))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let sha1 = crypto::sha1_file(path)?;
|
||||
|
||||
Ok(Self {
|
||||
sha1,
|
||||
meta,
|
||||
file_name,
|
||||
disabled: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type PackagesMap = HashMap<PackagePath, Package>;
|
||||
|
||||
pub struct Packages {
|
||||
managers: HashMap<ClusterPath, PackageManager>,
|
||||
}
|
||||
|
||||
impl Packages {
|
||||
#[must_use]
|
||||
pub fn initialize(clusters: &Clusters) -> Self {
|
||||
let mut this = Self { managers: HashMap::new() };
|
||||
|
||||
// TODO: This should probably not clone and store the cluster path in like 2 areas
|
||||
for cluster_path in clusters.0.keys() {
|
||||
this.add_cluster(cluster_path.clone());
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn add_cluster(&mut self, cluster_path: ClusterPath) {
|
||||
let mgr = PackageManager::new(cluster_path.clone());
|
||||
self.managers.insert(cluster_path, mgr);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get(&self, cluster_path: &ClusterPath) -> Option<&PackageManager> {
|
||||
self.managers.get(cluster_path)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, cluster_path: &ClusterPath) -> Option<&mut PackageManager> {
|
||||
self.managers.get_mut(cluster_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct PackagesMeta {
|
||||
pub packages: PackagesMap,
|
||||
pub package_type: PackageType,
|
||||
}
|
||||
|
||||
impl PackagesMeta {
|
||||
#[must_use]
|
||||
pub fn new(package_type: PackageType) -> Self {
|
||||
Self {
|
||||
packages: PackagesMap::new(),
|
||||
package_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PackageManager {
|
||||
pub cluster_path: ClusterPath,
|
||||
synced: bool,
|
||||
modpacks: PackagesMeta,
|
||||
mods: PackagesMeta,
|
||||
datapacks: PackagesMeta,
|
||||
resourcepacks: PackagesMeta,
|
||||
shaderpacks: PackagesMeta,
|
||||
}
|
||||
|
||||
impl PackageManager {
|
||||
#[must_use]
|
||||
pub fn new(cluster_path: ClusterPath) -> Self {
|
||||
Self {
|
||||
cluster_path,
|
||||
modpacks: PackagesMeta::new(PackageType::ModPack),
|
||||
synced: false,
|
||||
mods: PackagesMeta::new(PackageType::Mod),
|
||||
datapacks: PackagesMeta::new(PackageType::DataPack),
|
||||
resourcepacks: PackagesMeta::new(PackageType::ResourcePack),
|
||||
shaderpacks: PackagesMeta::new(PackageType::ShaderPack),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_meta_file(
|
||||
&self,
|
||||
dirs: &Directories,
|
||||
package_type: PackageType,
|
||||
) -> crate::Result<PathBuf> {
|
||||
Ok(self
|
||||
.cluster_path
|
||||
.full_path_dirs(dirs)
|
||||
.await?
|
||||
.join(package_type.get_meta()))
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
async fn get_and_sync(&mut self, dirs: Option<&Directories>) -> &mut Self {
|
||||
if !self.synced {
|
||||
self.synced = true; // early set to prevent infinite loop
|
||||
|
||||
let state = State::get().await.expect("couldn't get state when syncing packages");
|
||||
let dirs = dirs.map_or_else(|| &state.directories, |dirs| dirs);
|
||||
|
||||
self.sync_packages(dirs).await;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the `PackagesMeta` for a specific package type. Syncs.
|
||||
#[must_use]
|
||||
pub async fn get(&mut self, package_type: PackageType) -> &PackagesMeta {
|
||||
self.get_and_sync(None).await;
|
||||
match package_type {
|
||||
PackageType::Mod => &self.mods,
|
||||
PackageType::DataPack => &self.datapacks,
|
||||
PackageType::ResourcePack => &self.resourcepacks,
|
||||
PackageType::ShaderPack => &self.shaderpacks,
|
||||
PackageType::ModPack => &self.modpacks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `PackagesMeta` for a specific package type. Does not sync.
|
||||
fn get_mut(&mut self, package_type: PackageType) -> &mut PackagesMeta {
|
||||
match package_type {
|
||||
PackageType::Mod => &mut self.mods,
|
||||
PackageType::ModPack => &mut self.modpacks,
|
||||
PackageType::DataPack => &mut self.datapacks,
|
||||
PackageType::ResourcePack => &mut self.resourcepacks,
|
||||
PackageType::ShaderPack => &mut self.shaderpacks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the PackagesMeta for a specific package type. Does not sync.
|
||||
#[tracing::instrument]
|
||||
async fn get_from_file(
|
||||
&self,
|
||||
dirs: &Directories,
|
||||
package_type: PackageType,
|
||||
) -> crate::Result<PackagesMeta> {
|
||||
let packages_meta = &self.get_meta_file(dirs, package_type).await?;
|
||||
|
||||
let meta: PackagesMeta = if packages_meta.exists() && packages_meta.is_file() {
|
||||
if let Ok(meta) = serde_json::from_slice(&io::read(packages_meta).await?) {
|
||||
meta
|
||||
} else {
|
||||
io::copy(packages_meta, packages_meta.with_extension(".json.bak")).await?;
|
||||
io::write(packages_meta, "{}").await?;
|
||||
PackagesMeta::new(package_type)
|
||||
}
|
||||
} else {
|
||||
io::write(packages_meta, "{}").await?;
|
||||
PackagesMeta::new(package_type)
|
||||
};
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
// add a package to the manager
|
||||
#[tracing::instrument(skip(self, package))]
|
||||
pub async fn add_package(
|
||||
&mut self,
|
||||
package_path: PackagePath,
|
||||
package: Package,
|
||||
) -> crate::Result<()> {
|
||||
let package_type = package_path.package_type()?;
|
||||
let packages = &mut self.get_mut(package_type).packages;
|
||||
insert_package_by_path(packages, package_path, package);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// remove a package to the manager
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn remove_package(
|
||||
&mut self,
|
||||
package_path: &PackagePath,
|
||||
) -> crate::Result<()> {
|
||||
let package_type = package_path.package_type()?;
|
||||
let full_path = package_path.full_path(&self.cluster_path).await?;
|
||||
|
||||
io::remove_file(full_path).await?;
|
||||
|
||||
let packages = &mut self.get_mut(package_type).packages;
|
||||
packages.remove(package_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// disable a package in the manager
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn set_package_enabled(
|
||||
&mut self,
|
||||
package_path: &PackagePath,
|
||||
enabled: bool,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let package_type = package_path.package_type()?;
|
||||
let full_path = package_path.full_path(&self.cluster_path).await?;
|
||||
|
||||
let packages = &mut self.get_mut(package_type).packages;
|
||||
|
||||
let was_disabled = full_path.extension().map_or(false, |e| e == "disabled");
|
||||
let mut new_path = full_path.clone();
|
||||
|
||||
let changed = if enabled && was_disabled {
|
||||
new_path.set_extension("") // removes .disabled
|
||||
} else if !enabled && !was_disabled {
|
||||
new_path.set_extension("jar.disabled") // adds .disabled
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if changed {
|
||||
io::rename(full_path, new_path.clone()).await?;
|
||||
|
||||
|
||||
if let Some(mut package) = packages.remove(package_path) {
|
||||
let file_name = new_path.file_name().ok_or_else(|| anyhow::anyhow!("couldn't get file name"))?.to_string_lossy().to_string();
|
||||
let package_path = PackagePath::new(package_type.clone(), file_name.clone())?;
|
||||
|
||||
package.disabled = !enabled;
|
||||
package.file_name = file_name;
|
||||
|
||||
packages.insert(package_path, package);
|
||||
}
|
||||
|
||||
self.sync_to_file_by_type(&state.directories, package_type).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// sync all packages
|
||||
#[tracing::instrument(skip(self, dirs))]
|
||||
pub async fn sync_packages(&mut self, dirs: &Directories) {
|
||||
tracing::info!("syncing packages for cluster {}", self.cluster_path);
|
||||
let package_types = vec![
|
||||
PackageType::Mod,
|
||||
PackageType::DataPack,
|
||||
PackageType::ResourcePack,
|
||||
PackageType::ShaderPack,
|
||||
];
|
||||
|
||||
for package_type in package_types {
|
||||
if let Err(err) = &self.sync_packages_by_type(dirs, package_type, None).await {
|
||||
tracing::error!("failed to sync package {}: {}", package_type.get_name(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// sync packages from the metafile
|
||||
#[tracing::instrument(skip(self, dirs))]
|
||||
async fn sync_from_file_by_type(
|
||||
&mut self,
|
||||
dirs: &Directories,
|
||||
package_type: PackageType,
|
||||
) -> crate::Result<()> {
|
||||
let packages_meta = self.get_from_file(dirs, package_type).await?;
|
||||
self.get_mut(package_type).packages = packages_meta.packages;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// sync packages from the manager to the metafile
|
||||
#[tracing::instrument(skip(self, dirs))]
|
||||
async fn sync_to_file_by_type(
|
||||
&mut self,
|
||||
dirs: &Directories,
|
||||
package_type: PackageType,
|
||||
) -> crate::Result<()> {
|
||||
let packages_meta = self.get(package_type).await;
|
||||
let packages_meta = serde_json::to_string(&packages_meta)?;
|
||||
|
||||
io::write(self.get_meta_file(dirs, package_type).await?, packages_meta).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// returns a list of packages that have a file but are not synced in the manager
|
||||
#[tracing::instrument(skip(self, dirs, packages))]
|
||||
async fn sync_packages_from_local(
|
||||
&self,
|
||||
dirs: &Directories,
|
||||
packages: &mut PackagesMap,
|
||||
package_type: PackageType,
|
||||
) -> crate::Result<()> {
|
||||
let mut files = io::read_dir(
|
||||
self.cluster_path
|
||||
.full_path_dirs(dirs)
|
||||
.await?
|
||||
.join(package_type.get_folder()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut packages_to_keep = HashSet::<PackagePath>::new();
|
||||
let mut packages_to_infer = HashSet::<PathBuf>::new();
|
||||
|
||||
let full_cluster_path = self.cluster_path.full_path().await?;
|
||||
|
||||
while let Some(file) = files.next_entry().await? {
|
||||
// Skip .packages.json meta file
|
||||
if file.file_name().to_string_lossy().starts_with(&".") || file.file_type().await?.is_dir()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let package_path = PackagePath::from_path(&file.path()).await?;
|
||||
|
||||
// Check if the file is in the packages list already
|
||||
if packages.get(&package_path).is_none() {
|
||||
// Package path is not in the packages list but exists on file system, lets try to infer it later on
|
||||
packages_to_infer.insert(full_cluster_path.join(package_path.relative_path()));
|
||||
}
|
||||
|
||||
// Add the package to the set to not remove it from the manager
|
||||
packages_to_keep.insert(package_path);
|
||||
}
|
||||
|
||||
// Infer any packages
|
||||
if !packages_to_infer.is_empty() {
|
||||
let ingress = init_ingress(crate::IngressType::InferPackages, 300.0, format!("inferring {package_type}s").as_str()).await?;
|
||||
|
||||
if let Some(not_found) = infer_provider(&ingress, Providers::Modrinth, package_type, packages, packages_to_infer, crypto::sha1_file).await {
|
||||
if let Some(not_found) = infer_provider(&ingress, Providers::SkyClient, package_type, packages, not_found, crypto::md5_file).await {
|
||||
if let Some(not_found) = infer_provider(&ingress, Providers::Curseforge, package_type, packages, not_found, |path| {
|
||||
let hash = crypto::murmur2_file(path)?;
|
||||
Ok(hash.to_string())
|
||||
}).await {
|
||||
for path in not_found {
|
||||
tracing::warn!("failed to infer package: {:?}", path);
|
||||
|
||||
let meta = PackageMetadata::Unknown;
|
||||
let package = Package::new(&path, meta)?;
|
||||
let package_path = PackagePath::from_path(&path).await?;
|
||||
|
||||
insert_package_by_path(packages, package_path, package);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = send_ingress(&ingress, 300.0, Some(format!("finished inferring {package_type}s").as_str())).await;
|
||||
}
|
||||
|
||||
if packages.len() != packages_to_keep.len() {
|
||||
packages.retain(|pkg_path, _| packages_to_keep.contains(pkg_path));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// sync packages in a cluster
|
||||
#[tracing::instrument(skip(self, dirs))]
|
||||
pub async fn sync_packages_by_type(
|
||||
&mut self,
|
||||
dirs: &Directories,
|
||||
package_type: PackageType,
|
||||
clear: Option<bool>,
|
||||
) -> crate::Result<()> {
|
||||
if clear.unwrap_or(false) {
|
||||
self.get_mut(package_type).packages.clear();
|
||||
}
|
||||
|
||||
// Clone the current packages and merge them with the local package list
|
||||
let mut packages = self.get(package_type).await.packages.clone();
|
||||
let mut new_packages = self.get_from_file(dirs, package_type).await?.packages;
|
||||
packages.extend(new_packages.drain());
|
||||
|
||||
// Sync the packages from FS
|
||||
self.sync_packages_from_local(dirs, &mut packages, package_type).await?;
|
||||
|
||||
// Finally store the new list in memory and on disk
|
||||
let stored_packages = &self.get_mut(package_type).packages;
|
||||
if stored_packages.len() != packages.len() {
|
||||
self.get_mut(package_type).packages = packages;
|
||||
self.sync_to_file_by_type(dirs, package_type).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_package_by_path(packages: &mut PackagesMap, path: PackagePath, mut package: Package) {
|
||||
if let Ok(file_name) = path.file_name() {
|
||||
let is_disabled = file_name.ends_with(".disabled");
|
||||
|
||||
if is_disabled {
|
||||
package.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
packages.insert(path, package);
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(packages, to_infer))]
|
||||
async fn infer_provider(
|
||||
ingress: &IngressId,
|
||||
provider: Providers,
|
||||
package_type: PackageType,
|
||||
packages: &mut PackagesMap,
|
||||
to_infer: HashSet<PathBuf>,
|
||||
hash_fn: fn(&PathBuf) -> crate::Result<String>,
|
||||
) -> Option<HashSet<PathBuf>> {
|
||||
let _ = send_ingress(ingress, 5.0, Some(format!("inferring {package_type}s from {provider}").as_str())).await;
|
||||
|
||||
let mut hash_map = HashMap::<String, PathBuf>::new();
|
||||
for path in &to_infer {
|
||||
if let Ok(hash) = hash_fn(path) {
|
||||
hash_map.insert(hash, path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let hash_len = hash_map.len();
|
||||
|
||||
let _ = send_ingress(ingress, 25.0, None).await;
|
||||
match provider.get_versions_by_hashes(hash_map.iter().map(|hash| hash.0.clone()).collect()).await {
|
||||
Ok(versions) => {
|
||||
|
||||
let found_all = (hash_len - versions.len()) == 0;
|
||||
|
||||
let havent_found = if found_all {
|
||||
None
|
||||
} else {
|
||||
Some(hash_map.iter().filter_map(|(hash, path)| {
|
||||
if versions.contains_key(hash) {
|
||||
None
|
||||
} else {
|
||||
Some(path.clone())
|
||||
}
|
||||
}).collect::<HashSet<PathBuf>>())
|
||||
};
|
||||
|
||||
let package_ids = versions.iter().map(|(hash, version)| (version.package_id.clone(), hash.clone())).collect::<HashMap<String, String>>();
|
||||
|
||||
let _ = send_ingress(ingress, 25.0, None).await;
|
||||
match provider.get_multiple(package_ids.clone().into_keys().collect::<Vec<String>>().as_slice()).await {
|
||||
Ok(managed_packages) => {
|
||||
for managed in managed_packages {
|
||||
let Some(hash) = package_ids.get(&managed.id) else { continue };
|
||||
|
||||
let Some(version) = versions.get(hash) else { continue; };
|
||||
|
||||
if let Some(path) = hash_map.get(hash) {
|
||||
let meta = PackageMetadata::Managed {
|
||||
package_id: managed.id,
|
||||
provider: provider.clone(),
|
||||
package_type,
|
||||
title: managed.title,
|
||||
version_id: version.id.clone(),
|
||||
version_formatted: version.version_display.clone(),
|
||||
mc_versions: Some(version.game_versions.clone()),
|
||||
icon_url: managed.icon_url
|
||||
};
|
||||
|
||||
let Ok(package_path) = PackagePath::from_path(path).await else {
|
||||
tracing::error!("failed to create package path for {:?}", path);
|
||||
continue;
|
||||
};
|
||||
|
||||
match Package::new(path, meta) {
|
||||
Ok(package) => {
|
||||
insert_package_by_path(packages, package_path, package);
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("failed to create package from {}: {}", provider, err);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("failed to get versions from {}: {}", provider, err);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = send_ingress(ingress, 45.0, Some(format!("infered {package_type}s from {provider}").as_str())).await;
|
||||
return havent_found;
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("failed to get versions hashes from {}: {}", provider, err);
|
||||
}
|
||||
};
|
||||
|
||||
let _ = send_ingress(ingress, 70.0, Some(format!("failed to infer {package_type}s from {provider}").as_str())).await;
|
||||
Some(to_infer)
|
||||
}
|
||||
|
||||
/// Metadata that represents a [`Package`].
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum PackageMetadata {
|
||||
Managed {
|
||||
package_id: String,
|
||||
provider: Providers,
|
||||
package_type: PackageType,
|
||||
title: String,
|
||||
version_id: String,
|
||||
version_formatted: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
mc_versions: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
icon_url: Option<String>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl PackageMetadata {
|
||||
#[must_use]
|
||||
pub fn from_managed_package(package: ManagedPackage, version: ManagedVersion) -> Self {
|
||||
Self::Managed {
|
||||
package_id: package.id,
|
||||
version_id: version.id,
|
||||
version_formatted: version.version_display,
|
||||
title: package.title,
|
||||
provider: package.provider,
|
||||
package_type: package.package_type,
|
||||
mc_versions: Some(version.game_versions),
|
||||
icon_url: package.icon_url,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_package_type(&self) -> Option<PackageType> {
|
||||
match self {
|
||||
Self::Managed { package_type, .. } => Some(*package_type),
|
||||
Self::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Universal metadata for any managed user from a Mod distribution platform.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ManagedUser {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub avatar_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub bio: Option<String>,
|
||||
#[serde(default = "default_is_organization_user")]
|
||||
pub is_organization_user: bool,
|
||||
#[serde(default)]
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
const fn default_is_organization_user() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum Author {
|
||||
Team {
|
||||
team: String,
|
||||
organization: Option<String>,
|
||||
},
|
||||
Users(Vec<ManagedUser>),
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum PackageBody {
|
||||
Url(String),
|
||||
Markdown(String),
|
||||
}
|
||||
|
||||
/// Universal metadata for any managed package from a Mod distribution platform.
|
||||
#[serde_with::serde_as]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ManagedPackage {
|
||||
// Core Metadata
|
||||
pub provider: Providers,
|
||||
pub id: String,
|
||||
pub package_type: PackageType,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub body: PackageBody,
|
||||
pub main: String,
|
||||
pub versions: Vec<String>,
|
||||
pub game_versions: Vec<String>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub loaders: Vec<Loader>,
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
pub created: Option<DateTime<Utc>>,
|
||||
pub updated: Option<DateTime<Utc>>,
|
||||
pub client: PackageSide,
|
||||
pub server: PackageSide,
|
||||
pub downloads: u64,
|
||||
pub followers: u32,
|
||||
pub categories: Vec<String>,
|
||||
pub optional_categories: Option<Vec<String>>,
|
||||
pub license: Option<License>,
|
||||
pub author: Author,
|
||||
|
||||
pub is_archived: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct License {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
/// Universal managed package version of a package.
|
||||
#[serde_with::serde_as]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ManagedVersion {
|
||||
pub id: String,
|
||||
pub package_id: String,
|
||||
pub author: String,
|
||||
pub name: String,
|
||||
|
||||
pub featured: bool,
|
||||
pub version_display: String,
|
||||
pub changelog: String,
|
||||
pub changelog_url: Option<String>,
|
||||
|
||||
pub published: Option<DateTime<Utc>>,
|
||||
pub downloads: u32,
|
||||
pub version_type: ManagedVersionReleaseType,
|
||||
|
||||
pub files: Vec<ManagedVersionFile>,
|
||||
pub is_available: bool,
|
||||
pub deps: Vec<ManagedDependency>,
|
||||
pub game_versions: Vec<String>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub loaders: Vec<Loader>,
|
||||
}
|
||||
|
||||
impl ManagedVersion {
|
||||
#[must_use]
|
||||
pub fn get_primary_file(&self) -> Option<&ManagedVersionFile> {
|
||||
self.files.iter().find(|f| f.primary)
|
||||
}
|
||||
}
|
||||
|
||||
/// Universal version release type
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum ManagedVersionReleaseType {
|
||||
#[default]
|
||||
Release,
|
||||
Alpha,
|
||||
Beta,
|
||||
Snapshot,
|
||||
}
|
||||
|
||||
impl From<String> for ManagedVersionReleaseType {
|
||||
fn from(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"alpha" => Self::Alpha,
|
||||
"beta" => Self::Beta,
|
||||
"snapshot" => Self::Snapshot,
|
||||
_ => Self::Release,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ManagedVersionReleaseType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Release => "Release",
|
||||
Self::Alpha => "Alpha",
|
||||
Self::Beta => "Beta",
|
||||
Self::Snapshot => "Snapshot",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Universal interface for managed package files.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||
pub struct ManagedVersionFile {
|
||||
pub url: String,
|
||||
pub file_name: String,
|
||||
pub primary: bool,
|
||||
|
||||
pub size: u64,
|
||||
pub file_type: Option<PackageFile>,
|
||||
pub hashes: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Universal interface for managed package dependencies.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ManagedDependency {
|
||||
pub version_id: Option<String>,
|
||||
pub package_id: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub dependency_type: PackageDependency,
|
||||
}
|
||||
|
||||
/// The type of a [`ManagedDependency`].
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum PackageDependency {
|
||||
Required,
|
||||
Optional,
|
||||
Incompatible,
|
||||
Embedded,
|
||||
}
|
||||
|
||||
/// The Client/Server side type of a [`Package`].
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub enum PackageSide {
|
||||
Required,
|
||||
Optional,
|
||||
Unsupported,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// The file type of a [`Package`].
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PackageFile {
|
||||
RequiredPack,
|
||||
OptionalPack,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(io_semaphore))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn read_icon(
|
||||
icon_path: Option<String>,
|
||||
cache_dir: &Path,
|
||||
path: &PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Option<PathBuf>> {
|
||||
if let Some(icon_path) = icon_path {
|
||||
let zip_reader = ZipFileReader::new(path).await;
|
||||
if let Ok(zip_reader) = zip_reader {
|
||||
let zip_idx_res = zip_reader
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.position(|f| f.filename().as_str().unwrap_or_default() == icon_path);
|
||||
|
||||
if let Some(zip_idx) = zip_idx_res {
|
||||
let mut bytes = Vec::new();
|
||||
if zip_reader
|
||||
.reader_with_entry(zip_idx)
|
||||
.await?
|
||||
.read_to_end_checked(&mut bytes)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
let bytes = bytes::Bytes::from(bytes);
|
||||
let path = write_icon(&icon_path, cache_dir, bytes, io_semaphore).await?;
|
||||
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
|
@ -1,762 +0,0 @@
|
|||
//! Handles child processes in a unified API
|
||||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::cluster;
|
||||
use crate::constants::PROCESSOR_FILE;
|
||||
use crate::proxy::send::{send_message, send_process};
|
||||
use crate::proxy::ProcessPayloadType;
|
||||
use crate::utils::http::read_json;
|
||||
use crate::State;
|
||||
use onelauncher_utils::io::IOError;
|
||||
|
||||
use super::{Cluster, ClusterPath};
|
||||
|
||||
/// Wrapper over a `HashMap` of PIDs to `ProcessorChildren` and unified apis
|
||||
pub struct Processor(HashMap<Uuid, Arc<RwLock<ProcessorChild>>>);
|
||||
|
||||
/// Manager for safe processes like ingress feeds which need to be safely shut down.
|
||||
pub struct IngressProcessor {
|
||||
/// A vector of ingress feed uuids.
|
||||
pub ingress_feeds: Vec<Uuid>,
|
||||
}
|
||||
|
||||
/// A type of Ingress safety process.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum IngressProcessType {
|
||||
/// A single Ingress feed.
|
||||
IngressFeed,
|
||||
// Writes,
|
||||
// Reads,
|
||||
// Fetch,
|
||||
}
|
||||
|
||||
impl Default for IngressProcessor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl IngressProcessor {
|
||||
/// Initialize a new ingresss process manager.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
ingress_feeds: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an ingress safety process to the `HashMap`.
|
||||
pub async fn add_ingress(r#type: IngressProcessType, uuid: Uuid) -> crate::Result<Uuid> {
|
||||
let state = State::get().await?;
|
||||
let mut ingress_processor = state.ingress_processor.write().await;
|
||||
match r#type {
|
||||
IngressProcessType::IngressFeed => {
|
||||
ingress_processor.ingress_feeds.push(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(uuid)
|
||||
}
|
||||
|
||||
/// Complete and retain an ingress safety process from the `HashMap` of ingress feeds.
|
||||
pub async fn finish(r#type: IngressProcessType, uuid: Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut ingress_processor = state.ingress_processor.write().await;
|
||||
match r#type {
|
||||
IngressProcessType::IngressFeed => {
|
||||
ingress_processor.ingress_feeds.retain(|f| *f != uuid);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finish and check if an ingress safety processes ingress feed is empty and finished.
|
||||
pub async fn finished(r#type: IngressProcessType) -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let ingress_processor = state.ingress_processor.read().await;
|
||||
match r#type {
|
||||
IngressProcessType::IngressFeed => {
|
||||
if ingress_processor.ingress_feeds.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// A child process type.
|
||||
#[derive(Debug)]
|
||||
pub enum ChildType {
|
||||
/// A tokio-managed child process.
|
||||
ChildProcess(Child),
|
||||
/// A restored child from a process cache that needs to be re-registered.
|
||||
RescuedChild(u32),
|
||||
}
|
||||
|
||||
/// A structure representing a cache of a running process.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ProcessorCache {
|
||||
/// The PID of a process.
|
||||
pub pid: u32,
|
||||
/// The stored UUID of a process.
|
||||
pub uuid: Uuid,
|
||||
/// The time a process started.
|
||||
pub start_time: u64,
|
||||
/// The name of a process.
|
||||
pub name: String,
|
||||
/// The executable file running the process.
|
||||
pub exe: String,
|
||||
/// The associated [`ClusterPath`] to a process.
|
||||
pub cluster_path: ClusterPath,
|
||||
/// A post hook to be run once a process has completed.
|
||||
pub post: Option<String>,
|
||||
/// The [`Uuid`] of the Minecraft account it was started by.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl ChildType {
|
||||
/// Get the PID of a specific `ChildType` process.
|
||||
#[must_use]
|
||||
pub fn id(&self) -> Option<u32> {
|
||||
match self {
|
||||
Self::ChildProcess(child) => child.id(),
|
||||
Self::RescuedChild(pid) => Some(*pid),
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a cache of a specific `ChildType`.
|
||||
pub async fn cache(
|
||||
&self,
|
||||
uuid: Uuid,
|
||||
cluster_path: ClusterPath,
|
||||
post: Option<String>,
|
||||
user: Option<Uuid>,
|
||||
) -> crate::Result<()> {
|
||||
let pid = match self {
|
||||
Self::ChildProcess(child) => child.id().unwrap_or(0),
|
||||
Self::RescuedChild(pid) => *pid,
|
||||
};
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut system = sysinfo::System::new();
|
||||
|
||||
system.refresh_processes(sysinfo::ProcessesToUpdate::All);
|
||||
let process = system
|
||||
.process(sysinfo::Pid::from_u32(pid))
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find process {}", pid))?;
|
||||
let start_time = process.start_time();
|
||||
let name = process.name().to_string_lossy().to_string();
|
||||
let Some(path) = process.exe() else {
|
||||
return Err(anyhow::anyhow!("cached process {} has no path", pid).into());
|
||||
};
|
||||
|
||||
let exe = path.to_string_lossy().to_string();
|
||||
let cached_proccess = ProcessorCache {
|
||||
pid,
|
||||
uuid,
|
||||
start_time,
|
||||
name,
|
||||
exe,
|
||||
cluster_path,
|
||||
post,
|
||||
user,
|
||||
};
|
||||
|
||||
let path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
|
||||
let mut caches = (read_json::<HashMap<Uuid, ProcessorCache>>(&path, &state.io_semaphore)
|
||||
.await)
|
||||
.unwrap_or_default();
|
||||
|
||||
caches.insert(uuid, cached_proccess);
|
||||
crate::utils::http::write(&path, &serde_json::to_vec(&caches)?, &state.io_semaphore)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a child from the global store of processes.
|
||||
pub async fn remove(&self, uuid: Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
|
||||
let mut caches = (read_json::<HashMap<Uuid, ProcessorCache>>(&path, &state.io_semaphore)
|
||||
.await)
|
||||
.unwrap_or_default();
|
||||
caches.remove(&uuid);
|
||||
crate::utils::http::write(&path, &serde_json::to_vec(&caches)?, &state.io_semaphore)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kill a process in the global store of processes.
|
||||
pub async fn kill(&mut self) -> crate::Result<()> {
|
||||
match self {
|
||||
Self::ChildProcess(child) => Ok(child.kill().await.map_err(IOError::from)?),
|
||||
Self::RescuedChild(pid) => {
|
||||
let mut system = sysinfo::System::new();
|
||||
if system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[
|
||||
sysinfo::Pid::from_u32(*pid),
|
||||
])) != 0
|
||||
{
|
||||
let process = system.process(sysinfo::Pid::from_u32(*pid));
|
||||
if let Some(process) = process {
|
||||
process.kill();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for a process to complete.
|
||||
pub fn try_wait(&mut self) -> crate::Result<Option<i32>> {
|
||||
match self {
|
||||
Self::ChildProcess(child) => Ok(child
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.map(|x| x.code().unwrap_or(0))),
|
||||
Self::RescuedChild(pid) => {
|
||||
let mut system = sysinfo::System::new();
|
||||
if system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[
|
||||
sysinfo::Pid::from_u32(*pid),
|
||||
])) == 0
|
||||
{
|
||||
return Ok(Some(0));
|
||||
}
|
||||
|
||||
system.process(sysinfo::Pid::from_u32(*pid)).map_or_else(
|
||||
|| Ok(Some(0)),
|
||||
|process| {
|
||||
if process.status() == sysinfo::ProcessStatus::Run {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(0))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Processor`] child process.
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessorChild {
|
||||
/// The uuid of a process.
|
||||
pub uuid: Uuid,
|
||||
/// The associated [`ClusterPath`] to a process.
|
||||
pub cluster_path: ClusterPath,
|
||||
/// The process manager managing this process.
|
||||
pub manager: Option<JoinHandle<crate::Result<i32>>>,
|
||||
/// The [`ChildType`] of this process.
|
||||
pub current_child: Arc<RwLock<ChildType>>,
|
||||
/// When this process was last updated in [`Utc`].
|
||||
pub last_updated: DateTime<Utc>,
|
||||
/// When this process was last updated in [`Utc`].
|
||||
pub started_at: DateTime<Utc>,
|
||||
/// What [`Uuid`] this process is running under.
|
||||
pub user: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
/// Get a new [`Processor`] instance.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
/// Returns a reference to the value corresponding to the key.
|
||||
#[must_use]
|
||||
pub fn get(&self, uuid: Uuid) -> Option<Arc<RwLock<ProcessorChild>>> {
|
||||
self.0.get(&uuid).cloned()
|
||||
}
|
||||
|
||||
/// An collection visiting all keys in arbitrary order.
|
||||
#[must_use]
|
||||
pub fn keys(&self) -> Vec<Uuid> {
|
||||
self.0.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Get all running processes.
|
||||
pub async fn running(&self) -> crate::Result<Vec<Uuid>> {
|
||||
let mut keys = Vec::new();
|
||||
for key in self.keys() {
|
||||
if let Some(process) = self.get(key) {
|
||||
let process = process.clone();
|
||||
let process = process.write().await;
|
||||
if process.current_child.write().await.try_wait()?.is_none() {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Get all running cluster processes.
|
||||
pub async fn running_cluster(&self, cluster_path: ClusterPath) -> crate::Result<Vec<Uuid>> {
|
||||
let running = self.running().await?;
|
||||
let mut keys = Vec::new();
|
||||
for key in running {
|
||||
if let Some(process) = self.get(key) {
|
||||
let process = process.clone();
|
||||
let process = process.read().await;
|
||||
if process.cluster_path == cluster_path {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// get all running cluster paths.
|
||||
pub async fn running_cluster_paths(&self) -> crate::Result<Vec<ClusterPath>> {
|
||||
let mut clusters = Vec::new();
|
||||
for key in self.keys() {
|
||||
if let Some(process) = self.get(key) {
|
||||
let process = process.clone();
|
||||
let process = process.write().await;
|
||||
if process.current_child.write().await.try_wait()?.is_none() {
|
||||
clusters.push(process.cluster_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(clusters)
|
||||
}
|
||||
|
||||
/// get all running clusters.
|
||||
pub async fn running_clusters(&self) -> crate::Result<Vec<Cluster>> {
|
||||
let mut clusters = Vec::new();
|
||||
for key in self.keys() {
|
||||
if let Some(process) = self.get(key) {
|
||||
let process = process.clone();
|
||||
let process = process.write().await;
|
||||
if process.current_child.write().await.try_wait()?.is_none() {
|
||||
if let Some(cluster) = cluster::get(&process.cluster_path.clone()).await? {
|
||||
clusters.push(cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(clusters)
|
||||
}
|
||||
|
||||
/// get the exit status of a process by its [`Uuid`].
|
||||
pub async fn exit_status(&self, uuid: Uuid) -> crate::Result<Option<i32>> {
|
||||
if let Some(process) = self.get(uuid) {
|
||||
let process = process.write().await;
|
||||
let status = process.current_child.write().await.try_wait()?;
|
||||
Ok(status)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// restore processes from the cache.
|
||||
pub async fn restore(&mut self) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let processor_path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
|
||||
let mut processor_caches = if let Ok(processes_json) =
|
||||
read_json::<HashMap<Uuid, ProcessorCache>>(&processor_path, &state.io_semaphore).await
|
||||
{
|
||||
let zeros = HashMap::<Uuid, ProcessorCache>::new();
|
||||
crate::utils::http::write(
|
||||
&processor_path,
|
||||
&serde_json::to_vec(&zeros)?,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
processes_json
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
for (_, cache) in processor_caches.drain() {
|
||||
let uuid = cache.uuid;
|
||||
match self.insert_restored(cache).await {
|
||||
Ok(process) => {
|
||||
self.0.insert(uuid, process);
|
||||
}
|
||||
Err(e) => tracing::warn!("failed to restore process {}: {}", uuid, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// insert a process with a command and [`ClusterPath`].
|
||||
#[tracing::instrument(skip(self, uuid, command, post, censors))]
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
#[onelauncher_macros::memory]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn insert_process(
|
||||
&mut self,
|
||||
uuid: Uuid,
|
||||
cluster_path: ClusterPath,
|
||||
mut command: Command,
|
||||
post: Option<String>,
|
||||
censors: HashMap<String, String>,
|
||||
user: Option<Uuid>,
|
||||
enable_gamemode: Option<bool>,
|
||||
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
|
||||
command
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.stdin(Stdio::null());
|
||||
|
||||
let mut proc = command.spawn().map_err(IOError::from)?;
|
||||
|
||||
let pid = proc
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process failed, couldn't get pid"))?;
|
||||
|
||||
let stdout = proc.stdout.take().unwrap();
|
||||
let stderr = proc.stderr.take().unwrap();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if enable_gamemode.is_some_and(|x| x) {
|
||||
if let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid) {
|
||||
tracing::warn!("failed to enable gamemode, continuing: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut stdout = BufReader::new(stdout).lines();
|
||||
let mut stderr = BufReader::new(stderr).lines();
|
||||
|
||||
while let Ok(Some(line)) = tokio::select! {
|
||||
line = stdout.next_line() => line,
|
||||
line = stderr.next_line() => line,
|
||||
} {
|
||||
let mut censored = line.clone();
|
||||
for (key, value) in &censors {
|
||||
censored = censored.replace(key, value);
|
||||
}
|
||||
|
||||
if let Err(err) =
|
||||
send_process(uuid, pid, ProcessPayloadType::Logging, &censored).await
|
||||
{
|
||||
tracing::warn!("failed to send process log: {}", err);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let process = ChildType::ChildProcess(proc);
|
||||
|
||||
process
|
||||
.cache(uuid, cluster_path.clone(), post.clone(), user)
|
||||
.await?;
|
||||
|
||||
let current_child = Arc::new(RwLock::new(process));
|
||||
let manager = Some(tokio::spawn(Self::manager(
|
||||
uuid,
|
||||
post,
|
||||
pid,
|
||||
current_child.clone(),
|
||||
cluster_path.clone(),
|
||||
)));
|
||||
|
||||
send_process(uuid, pid, ProcessPayloadType::Started, "started process").await?;
|
||||
let last_updated = Utc::now();
|
||||
let child = ProcessorChild {
|
||||
uuid,
|
||||
cluster_path,
|
||||
current_child,
|
||||
manager,
|
||||
last_updated,
|
||||
started_at: Utc::now(),
|
||||
user,
|
||||
};
|
||||
let child = Arc::new(RwLock::new(child));
|
||||
self.0.insert(uuid, child.clone());
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
/// Insert a cached process with a [`ProcessorCache`].
|
||||
#[tracing::instrument(skip(self, cache))]
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn insert_restored(
|
||||
&mut self,
|
||||
cache: ProcessorCache,
|
||||
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
|
||||
let _state = State::get().await?;
|
||||
|
||||
// ensure located stray process matches with our pid recorded process
|
||||
{
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_processes(sysinfo::ProcessesToUpdate::All);
|
||||
let process = system
|
||||
.process(sysinfo::Pid::from_u32(cache.pid))
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find pid {}", cache.pid))?;
|
||||
|
||||
if cache.start_time != process.start_time() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"restored process {} has a mismatched start time {}",
|
||||
cache.pid,
|
||||
process.start_time()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if cache.name != process.name().to_string_lossy() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"restored process {} has a mismatched name {}",
|
||||
cache.pid,
|
||||
process.name().to_string_lossy().to_string()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if let Some(path) = process.exe() {
|
||||
if cache.exe != path.to_string_lossy() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"restored process {} has a mismatched exe {}",
|
||||
cache.pid,
|
||||
path.to_string_lossy()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
return Err(
|
||||
anyhow::anyhow!("restored process {} has no exe path", cache.pid).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let process = ChildType::RescuedChild(cache.pid);
|
||||
let pid = process
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?;
|
||||
process
|
||||
.cache(
|
||||
cache.uuid,
|
||||
cache.cluster_path.clone(),
|
||||
cache.post.clone(),
|
||||
cache.user,
|
||||
)
|
||||
.await?;
|
||||
let current_child = Arc::new(RwLock::new(process));
|
||||
let manager = Some(tokio::spawn(Self::manager(
|
||||
cache.uuid,
|
||||
cache.post,
|
||||
pid,
|
||||
current_child.clone(),
|
||||
cache.cluster_path.clone(),
|
||||
)));
|
||||
|
||||
send_process(
|
||||
cache.uuid,
|
||||
pid,
|
||||
ProcessPayloadType::Started,
|
||||
"started process",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let last_updated = Utc::now();
|
||||
let child = ProcessorChild {
|
||||
uuid: cache.uuid,
|
||||
cluster_path: cache.cluster_path,
|
||||
started_at: Utc
|
||||
.timestamp_opt(cache.start_time as i64, 0)
|
||||
.single()
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("couldn't convert processor cache timestamp to Utc")
|
||||
})?, // TODO: Look into this
|
||||
current_child,
|
||||
manager,
|
||||
last_updated,
|
||||
user: cache.user,
|
||||
};
|
||||
|
||||
let child = Arc::new(RwLock::new(child));
|
||||
self.0.insert(cache.uuid, child.clone());
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
/// Get a process manager and runner awaiting for the exit status.
|
||||
#[tracing::instrument(skip(current_child))]
|
||||
#[onelauncher_macros::memory]
|
||||
async fn manager(
|
||||
uuid: Uuid,
|
||||
post: Option<String>,
|
||||
mut current_pid: u32,
|
||||
current_child: Arc<RwLock<ChildType>>,
|
||||
cluster_path: ClusterPath,
|
||||
) -> crate::Result<i32> {
|
||||
let current_child = current_child.clone();
|
||||
let mut exit_status;
|
||||
let mut last_updated = Utc::now();
|
||||
|
||||
// core main process loop, managed by tokio
|
||||
loop {
|
||||
let value = current_child.write().await.try_wait();
|
||||
if let Some(stat) = value? {
|
||||
exit_status = stat;
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
let update = Utc::now().signed_duration_since(last_updated).num_seconds();
|
||||
if update >= 60 {
|
||||
if let Err(err) = cluster::edit(&cluster_path, |cluster| {
|
||||
cluster.meta.recently_played += update as u64;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"failed to update playtime for cluster {}: {}",
|
||||
&cluster_path,
|
||||
err
|
||||
);
|
||||
}
|
||||
last_updated = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
let update = Utc::now().signed_duration_since(last_updated).num_seconds();
|
||||
if let Err(err) = cluster::edit(&cluster_path, |cluster| {
|
||||
cluster.meta.recently_played += update as u64;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"failed to update playtime for cluster {}: {}",
|
||||
&cluster_path,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
let cluster_path_in = cluster_path.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = cluster::update_playtime(&cluster_path_in).await {
|
||||
tracing::warn!(
|
||||
"failed to update playtime for cluster {}: {}",
|
||||
&cluster_path_in,
|
||||
err
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async {
|
||||
let state = State::get().await;
|
||||
let state = match state {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to get state: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let _ = state.discord_rpc.clear(true).await;
|
||||
});
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let window = crate::ProxyState::get_main_window().await?;
|
||||
window.unminimize()?;
|
||||
}
|
||||
|
||||
{
|
||||
let current_child = current_child.write().await;
|
||||
current_child.remove(uuid).await?;
|
||||
}
|
||||
|
||||
if !exit_status == 0 {
|
||||
send_process(
|
||||
uuid,
|
||||
current_pid,
|
||||
ProcessPayloadType::Finished,
|
||||
"exited process",
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(exit_status);
|
||||
}
|
||||
|
||||
let post = if let Some(hook) = post {
|
||||
if hook.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut cmd = hook.split(' ');
|
||||
if let Some(c) = cmd.next() {
|
||||
let mut c = Command::new(c);
|
||||
c.args(cmd).current_dir(cluster_path.full_path().await?);
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(mut m_c) = post {
|
||||
let result = {
|
||||
{
|
||||
let mut current_child: tokio::sync::RwLockWriteGuard<'_, ChildType> =
|
||||
current_child.write().await;
|
||||
|
||||
let new_child = m_c.spawn().map_err(IOError::from)?;
|
||||
|
||||
current_pid = new_child
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?;
|
||||
|
||||
*current_child = ChildType::ChildProcess(new_child);
|
||||
};
|
||||
|
||||
send_process(
|
||||
uuid,
|
||||
current_pid,
|
||||
ProcessPayloadType::Modified,
|
||||
"running post hook",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let value = current_child.write().await.try_wait();
|
||||
|
||||
if let Ok(Some(stat)) = value {
|
||||
exit_status = stat;
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
tracing::error!("failed to run post hook: {err}");
|
||||
let _ = send_message(format!("failed to run post hook: {err}").as_str()).await;
|
||||
}
|
||||
};
|
||||
|
||||
send_process(
|
||||
uuid,
|
||||
current_pid,
|
||||
ProcessPayloadType::Finished,
|
||||
"exited process",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(exit_status)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Processor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
//! Handles user-inputted settings and global values
|
||||
|
||||
use super::{Directories, JavaVersions};
|
||||
use crate::constants::CURRENT_SETTINGS_FORMAT_VERSION;
|
||||
use onelauncher_utils::io;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A global settings state for the launcher.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Settings {
|
||||
/// A `OneLauncher` theme managed by the `OneLauncher` GUI.
|
||||
#[serde(default = "theme_default")]
|
||||
pub theme: String,
|
||||
/// A global browser list view for the `OneLauncher` GUI.
|
||||
#[serde(default)]
|
||||
pub browser_list_view: BrowserListView,
|
||||
/// Does not ask for confirmation when closing the `OneLauncher` GUI
|
||||
#[serde(default)]
|
||||
pub hide_close_prompt: bool,
|
||||
/// Disables animations in the `OneLauncher` GUI
|
||||
#[serde(default)]
|
||||
pub disable_animations: bool,
|
||||
/// A global fullscreen Minecraft state.
|
||||
#[serde(default)]
|
||||
pub force_fullscreen: bool,
|
||||
/// Whether to allow launching the same cluster under the same account.
|
||||
#[serde(default)]
|
||||
pub allow_parallel_running_clusters: bool,
|
||||
/// Whether to launch Feral Gamemode on Linux systems.
|
||||
#[serde(default)]
|
||||
pub enable_gamemode: bool,
|
||||
/// A global default [`Resolution`] for Minecraft.
|
||||
pub resolution: Resolution,
|
||||
/// A global [`JavaVersions`] list and default version.
|
||||
pub java_versions: JavaVersions,
|
||||
/// A global [`Memory`] settings store for Java memory settings.
|
||||
pub memory: Memory,
|
||||
/// Global and default initialization hooks .
|
||||
pub init_hooks: InitHooks,
|
||||
/// Global and default custom Java arguments.
|
||||
pub custom_java_args: Vec<String>,
|
||||
/// Global and default custom environment variables.
|
||||
pub custom_env_args: Vec<(String, String)>,
|
||||
/// Global and default maximum [`Semaphore`] I/O operations.
|
||||
pub max_async_io_operations: usize,
|
||||
/// Global and default maximum [`Semaphore`] HTTP operations.
|
||||
pub max_async_fetches: usize,
|
||||
/// The [`CURRENT_FORMAT_VERSION`] of this settings file.
|
||||
pub version: u32,
|
||||
/// Whether or not to disable Discord IPC.
|
||||
#[serde(default)]
|
||||
pub disable_discord: bool,
|
||||
/// Whether or not to enable a debug mode in the launcher.
|
||||
#[serde(default)]
|
||||
pub debug_mode: bool,
|
||||
/// Whether or not to disable Plausible and Sentry analytics.
|
||||
#[serde(default)]
|
||||
pub disable_analytics: bool,
|
||||
/// The core global config directory stored as a [`PathBuf`].
|
||||
#[serde(default = "Directories::init_settings_dir")]
|
||||
pub config_dir: Option<PathBuf>,
|
||||
/// Whether or not to minimize the launcher upon a game launch.
|
||||
#[serde(default)]
|
||||
pub hide_on_launch: bool,
|
||||
/// Enable/disable custom window decorations.
|
||||
#[serde(default)]
|
||||
pub custom_frame: bool,
|
||||
/// Completed onboarding.
|
||||
#[serde(default)]
|
||||
pub onboarding_completed: bool,
|
||||
}
|
||||
|
||||
fn theme_default() -> String {
|
||||
"dark".to_string()
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Initializes the global settings state.
|
||||
#[tracing::instrument]
|
||||
pub async fn initialize(file: &Path) -> crate::Result<Self> {
|
||||
let mut recovered_corruption = false;
|
||||
|
||||
let settings = if file.exists() {
|
||||
let read_settings = io::read(&file)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("error reading settings file: {0}", err).into())
|
||||
.and_then(|it| serde_json::from_slice::<Self>(&it).map_err(crate::Error::from));
|
||||
|
||||
if let Err(ref err) = read_settings {
|
||||
tracing::error!("failed to load settings file: {err}.");
|
||||
let backup = file.with_extension("json.bak");
|
||||
tracing::error!(
|
||||
"corrupted settings will be backed up as {}, and a new one will be created",
|
||||
backup.display()
|
||||
);
|
||||
let _ = io::rename(file, backup).await;
|
||||
recovered_corruption = true;
|
||||
}
|
||||
|
||||
read_settings.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(settings) = settings {
|
||||
Ok(settings)
|
||||
} else {
|
||||
let settings = Self {
|
||||
theme: "dark".to_string(),
|
||||
browser_list_view: BrowserListView::Grid,
|
||||
hide_close_prompt: true,
|
||||
disable_animations: false,
|
||||
force_fullscreen: false,
|
||||
allow_parallel_running_clusters: false,
|
||||
#[cfg(target_os = "linux")]
|
||||
enable_gamemode: true,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
enable_gamemode: false,
|
||||
resolution: Resolution::default(),
|
||||
java_versions: JavaVersions::new(),
|
||||
memory: Memory::default(),
|
||||
init_hooks: InitHooks::default(),
|
||||
custom_env_args: Vec::new(),
|
||||
custom_java_args: Vec::new(),
|
||||
max_async_fetches: 10,
|
||||
max_async_io_operations: 10,
|
||||
version: CURRENT_SETTINGS_FORMAT_VERSION,
|
||||
disable_analytics: false,
|
||||
disable_discord: false,
|
||||
debug_mode: false,
|
||||
config_dir: Directories::init_settings_dir(),
|
||||
hide_on_launch: false,
|
||||
custom_frame: true,
|
||||
onboarding_completed: false,
|
||||
};
|
||||
|
||||
if recovered_corruption {
|
||||
settings.sync(file).await?;
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronizes the current settings from a file.
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||
io::write(to, serde_json::to_vec(self)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// `OneLauncher` browser list type.
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BrowserListView {
|
||||
#[default]
|
||||
Grid,
|
||||
List,
|
||||
}
|
||||
|
||||
/// Global memory settings across all clusters.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct Memory {
|
||||
/// Maximum amount of Java memory available globally.
|
||||
pub maximum: u32,
|
||||
/// Minimum amount of Java memory available globally.
|
||||
pub minimum: u32,
|
||||
}
|
||||
|
||||
impl Default for Memory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
maximum: 2048,
|
||||
minimum: 256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global Minecraft resolution.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct Resolution(pub u16, pub u16);
|
||||
|
||||
impl Default for Resolution {
|
||||
fn default() -> Self {
|
||||
Self(854, 480)
|
||||
}
|
||||
}
|
||||
|
||||
/// Global initialization hooks for all Minecraft clusters.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
pub struct InitHooks {
|
||||
/// Pre-launch hook.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pre: Option<String>,
|
||||
/// Wrapper hook for the runtime of the game instance.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wrapper: Option<String>,
|
||||
/// Post-launch hook.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub post: Option<String>,
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
pub fn sha1(bytes: &[u8]) -> String {
|
||||
sha1_smol::Sha1::from(bytes).digest().to_string()
|
||||
}
|
||||
|
||||
pub fn sha1_file(file: &PathBuf) -> Result<String> {
|
||||
Ok(sha1(&std::fs::read(file)?))
|
||||
}
|
||||
|
||||
pub fn sha1_files(files: Vec<&PathBuf>) -> Vec<Option<String>> {
|
||||
let mut hashes = Vec::new();
|
||||
for file in files {
|
||||
hashes.push(sha1_file(file).ok());
|
||||
}
|
||||
hashes
|
||||
}
|
||||
|
||||
pub fn md5(bytes: &[u8]) -> String {
|
||||
let digest = md5::compute(bytes);
|
||||
format!("{:x}", digest)
|
||||
}
|
||||
|
||||
pub fn md5_file(file: &PathBuf) -> Result<String> {
|
||||
Ok(md5(&std::fs::read(file)?))
|
||||
}
|
||||
|
||||
pub fn md5_files(files: Vec<&PathBuf>) -> Vec<Option<String>> {
|
||||
let mut hashes = Vec::new();
|
||||
for file in files {
|
||||
hashes.push(md5_file(file).ok());
|
||||
}
|
||||
hashes
|
||||
}
|
||||
|
||||
const CURSEFORGE_FINGERPRINT_SEED: u32 = 1;
|
||||
pub fn murmur2(bytes: &[u8]) -> u32 {
|
||||
let normalized = normalize_byte_array(bytes);
|
||||
murmur2::murmur2(normalized.as_slice(), CURSEFORGE_FINGERPRINT_SEED)
|
||||
}
|
||||
|
||||
pub fn murmur2_file(file: &PathBuf) -> Result<u32> {
|
||||
Ok(murmur2(&std::fs::read(file)?))
|
||||
}
|
||||
|
||||
pub fn murmur2_files(files: Vec<&PathBuf>) -> Vec<Option<u32>> {
|
||||
let mut hashes = Vec::new();
|
||||
for file in files {
|
||||
hashes.push(murmur2_file(file).ok());
|
||||
}
|
||||
hashes
|
||||
}
|
||||
|
||||
// Curseforge and their documented code :D (it wasn't documented)
|
||||
// https://github.com/CurseForgeCommunity/.NET-APIClient/blob/2c4f5f613d20025f9286fdd53592f8519022918f/Murmur2.cs#L8-L21
|
||||
// https://github.com/comp500/infra.link/blob/a298502fdb15b8939fe0effe71ed42ad06f3946f/murmur2/go/main.go#L10-L40
|
||||
fn normalize_byte_array(bytes: &[u8]) -> Vec<u8> {
|
||||
let normalized = bytes
|
||||
.iter()
|
||||
.filter(|byte| !matches!(byte, 9 | 10 | 13 | 32))
|
||||
.copied()
|
||||
.collect::<Vec<u8>>();
|
||||
// println!("Old len {}", bytes.len());
|
||||
// println!("New len {}", normalized.len());
|
||||
normalized
|
||||
}
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
//! **HTTP Utilities**
|
||||
//!
|
||||
//! Async extensions and wrappers around [`reqwest`] functions.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use bytes::Bytes;
|
||||
use reqwest::Method;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::{RwLock, Semaphore};
|
||||
|
||||
use crate::constants::FETCH_ATTEMPTS;
|
||||
use crate::proxy::send::send_ingress;
|
||||
use crate::proxy::IngressId;
|
||||
|
||||
use onelauncher_utils::io;
|
||||
use onelauncher_utils::io::IOError;
|
||||
|
||||
use super::crypto;
|
||||
|
||||
/// A [`Semaphore`] used for all I/O operations.
|
||||
#[derive(Debug)]
|
||||
pub struct IoSemaphore(pub RwLock<Semaphore>);
|
||||
/// A [`Semaphore`] used for all HTTP operations.
|
||||
#[derive(Debug)]
|
||||
pub struct FetchSemaphore(pub RwLock<Semaphore>);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// A public reqwest client with configured headers.
|
||||
pub static ref REQWEST_CLIENT: reqwest::Client = {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let header = reqwest::header::HeaderValue::from_str(&format!(
|
||||
"{}/{} (https://polyfrost.org)",
|
||||
crate::constants::NAME,
|
||||
crate::constants::VERSION,
|
||||
)).expect("failed to build reqwest headers!");
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(std::time::Duration::from_secs(15)))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.expect("failed to build reqwest client!")
|
||||
};
|
||||
}
|
||||
|
||||
/// Basic HTTP fetch interface.
|
||||
#[tracing::instrument(skip(semaphore))]
|
||||
pub async fn fetch(
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Bytes> {
|
||||
fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore).await
|
||||
}
|
||||
|
||||
/// Basic JSON fetch interface.
|
||||
#[tracing::instrument(skip(json_body, semaphore))]
|
||||
pub async fn fetch_json<T>(
|
||||
method: Method,
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
json_body: Option<serde_json::Value>,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let result = fetch_advanced(method, url, sha1, json_body, None, None, semaphore).await?;
|
||||
|
||||
let value = serde_json::from_slice(&result)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Advanced HTTP fetch interface with json, sha1 validation, and ingress support.
|
||||
#[tracing::instrument(skip(json_body, semaphore))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn fetch_advanced(
|
||||
method: Method,
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
json_body: Option<serde_json::Value>,
|
||||
headers: Option<HashMap<&str, &str>>,
|
||||
ingress: Option<(&IngressId, f64)>,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Bytes> {
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
|
||||
let mut req = REQWEST_CLIENT.request(method.clone(), url);
|
||||
|
||||
if let Some(body) = json_body.clone() {
|
||||
req = req.json(&body);
|
||||
}
|
||||
|
||||
if let Some(headers) = &headers {
|
||||
for (key, value) in headers {
|
||||
req = req.header(*key, *value);
|
||||
}
|
||||
}
|
||||
|
||||
let result = req.send().await;
|
||||
|
||||
match result {
|
||||
Ok(x) => {
|
||||
let bytes = if let Some((feed, total)) = &ingress {
|
||||
let length = x.content_length();
|
||||
if let Some(total_size) = length {
|
||||
use futures::StreamExt;
|
||||
let mut stream = x.bytes_stream();
|
||||
let mut bytes = Vec::new();
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk =
|
||||
item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?;
|
||||
bytes.append(&mut chunk.to_vec());
|
||||
send_ingress(
|
||||
feed,
|
||||
(chunk.len() as f64 / total_size as f64) * total,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Bytes::from(bytes))
|
||||
} else {
|
||||
x.bytes().await
|
||||
}
|
||||
} else {
|
||||
x.bytes().await
|
||||
};
|
||||
|
||||
if let Ok(bytes) = bytes {
|
||||
if let Some(sha1) = sha1 {
|
||||
let hash = crypto::sha1(&bytes);
|
||||
if &*hash != sha1 {
|
||||
if attempt <= 3 {
|
||||
continue;
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"hash {0} does not match {1}",
|
||||
sha1.to_string(),
|
||||
hash
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("finished downloading {url}");
|
||||
return Ok(bytes);
|
||||
} else if attempt <= 3 {
|
||||
continue;
|
||||
} else if let Err(err) = bytes {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
Err(_) if attempt <= 3 => continue,
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// A utility to fetch from multiple mirrored sources.
|
||||
#[tracing::instrument(skip(semaphore))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn fetch_from_mirrors(
|
||||
mirrors: &[&str],
|
||||
sha1: Option<&str>,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Bytes> {
|
||||
if mirrors.is_empty() {
|
||||
return Err(anyhow::anyhow!("no mirrors provided!").into());
|
||||
}
|
||||
|
||||
for (index, mirror) in mirrors.iter().enumerate() {
|
||||
let result = fetch(mirror, sha1, semaphore).await;
|
||||
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// Checks if we are playing offline by contacting a reliable server.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn check_internet_connection(timeout: u64) -> bool {
|
||||
REQWEST_CLIENT
|
||||
.get("https://api.polyfrost.org/")
|
||||
.timeout(std::time::Duration::from_secs(timeout))
|
||||
.send()
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Post JSON to a specified URL (helpful for Microsoft auth and Metadata)
|
||||
#[tracing::instrument(skip(json_body, semaphore))]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn post_json<T>(
|
||||
url: &str,
|
||||
json_body: serde_json::Value,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
let req = REQWEST_CLIENT.post(url).json(&json_body);
|
||||
|
||||
let result = req.send().await?.error_for_status()?;
|
||||
let value = result.json().await?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Read JSON from a specified [`Path`].
|
||||
pub async fn read_json<T>(path: &Path, semaphore: &IoSemaphore) -> crate::Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
let json = io::read(path).await?;
|
||||
let json = serde_json::from_slice::<T>(&json)?;
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Write to a file at a specified [`Path`].
|
||||
#[tracing::instrument(skip(bytes, semaphore))]
|
||||
pub async fn write<'a>(path: &Path, bytes: &[u8], semaphore: &IoSemaphore) -> crate::Result<()> {
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
io::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut file = tokio::fs::File::create(path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, path))?;
|
||||
file.write_all(bytes)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, path))?;
|
||||
tracing::trace!("done writing to file {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy a file from one [`AsRef<Path>`] to another.
|
||||
pub async fn copy(
|
||||
src: impl AsRef<Path>,
|
||||
dest: impl AsRef<Path>,
|
||||
semaphore: &IoSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
let src = src.as_ref();
|
||||
let dest = dest.as_ref();
|
||||
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
if let Some(parent) = dest.parent() {
|
||||
io::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
io::copy(src, dest).await?;
|
||||
tracing::trace!(
|
||||
"done copying file from {} to {}",
|
||||
src.display(),
|
||||
dest.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes, io_semaphore))]
|
||||
pub async fn write_icon(
|
||||
icon_path: &str,
|
||||
cache_dir: &Path,
|
||||
bytes: Bytes,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let ext = Path::new(&icon_path).extension().and_then(OsStr::to_str);
|
||||
let hash = crypto::sha1(&bytes);
|
||||
let path = cache_dir.join("icons").join(if let Some(e) = ext {
|
||||
format!("{hash}.{e}")
|
||||
} else {
|
||||
hash
|
||||
});
|
||||
|
||||
write(&path, &bytes, io_semaphore).await?;
|
||||
|
||||
let path = io::canonicalize(path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
|
@ -1,366 +0,0 @@
|
|||
//! **Java Utilities**
|
||||
//!
|
||||
//! Async utilities for managing and downloading Java versions.
|
||||
|
||||
use crate::constants::JAVA_BIN;
|
||||
use crate::State;
|
||||
use futures::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tokio::task::JoinError;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use winreg::{
|
||||
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY},
|
||||
RegKey,
|
||||
};
|
||||
|
||||
use onelauncher_utils::io;
|
||||
|
||||
/// A struct representing a single version of the Java Runtime Environment.
|
||||
/// Use [`locate_java`] to get an array of all located java instances on the machine.
|
||||
///
|
||||
/// common java paths from <https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/java/JavaUtils.cpp> under GPL 3.0
|
||||
///
|
||||
/// java check functionality from <https://github.com/modrinth/theseus/blob/master/theseus/src/util/jre.rs> under MIT
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)]
|
||||
pub struct JavaVersion {
|
||||
pub version: String,
|
||||
pub arch: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Java entrypoint: Returns all possibly existing java paths on a Linux machine and validates them,
|
||||
/// returning a [`Vec<JavaVersion>`] with all valid java installations, their paths, and associated metadata.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tracing::instrument]
|
||||
pub async fn locate_java() -> Result<Vec<JavaVersion>, JavaError> {
|
||||
let mut paths = HashSet::new();
|
||||
paths.extend(locate_java_path().await);
|
||||
paths.extend(locate_installed_java().await?);
|
||||
if let Ok(java_home) = env::var("JAVA_HOME") {
|
||||
paths.insert(PathBuf::from(java_home));
|
||||
}
|
||||
|
||||
let common_java_paths = [
|
||||
r"C:/Program Files/Java",
|
||||
r"C:\Program Files\Eclipse Adoptium",
|
||||
r"C:/Program Files (x86)/Java",
|
||||
r"C:\Program Files (x86)/Eclipse Adoptium",
|
||||
];
|
||||
|
||||
for path in common_java_paths {
|
||||
let Ok(java_subs) = std::fs::read_dir(path) else {
|
||||
continue;
|
||||
};
|
||||
for java_sub in java_subs.flatten() {
|
||||
let full_path = java_sub.path();
|
||||
paths.insert(full_path.join("bin"));
|
||||
}
|
||||
}
|
||||
|
||||
let common_key_paths = [
|
||||
r"SOFTWARE\\JavaSoft\\Java Runtime Environment",
|
||||
r"SOFTWARE\\JavaSoft\\Java Development Kit",
|
||||
r"SOFTWARE\\JavaSoft\\JRE",
|
||||
r"SOFTWARE\\JavaSoft\\JDK",
|
||||
r"SOFTWARE\\AdoptOpenJDK\\JRE",
|
||||
r"SOFTWARE\\AdoptOpenJDK\\JDK",
|
||||
r"SOFTWARE\\Eclipse Foundation\\JDK",
|
||||
r"SOFTWARE\\Eclipse Foundation\\JRE",
|
||||
r"SOFTWARE\\Eclipse Adoptium\\JRE",
|
||||
r"SOFTWARE\\Eclipse Adoptium\\JDK",
|
||||
r"SOFTWARE\\Microsoft\\JDK",
|
||||
r"SOFTWARE\\Azul Systems\\Zulu",
|
||||
r"SOFTWARE\\BellSoft\\Liberica",
|
||||
];
|
||||
|
||||
for key in common_key_paths {
|
||||
if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE)
|
||||
.open_subkey_with_flags(key, KEY_READ | KEY_WOW64_32KEY)
|
||||
{
|
||||
paths.extend(get_java_paths_from_windows(java_key));
|
||||
}
|
||||
if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE)
|
||||
.open_subkey_with_flags(key, KEY_READ | KEY_WOW64_64KEY)
|
||||
{
|
||||
paths.extend(get_java_paths_from_windows(java_key));
|
||||
}
|
||||
}
|
||||
|
||||
let java = check_java(paths).await.into_iter().collect();
|
||||
|
||||
Ok(java)
|
||||
}
|
||||
|
||||
/// Locates possibly existing java paths on Windows from Registry keys, returning as a [`HashSet<PathBuf>`]
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tracing::instrument]
|
||||
pub fn get_java_paths_from_windows(java_key: RegKey) -> HashSet<PathBuf> {
|
||||
let mut paths = HashSet::new();
|
||||
|
||||
for subkey in java_key.enum_keys().flatten() {
|
||||
if let Ok(subkey) = java_key.open_subkey(subkey) {
|
||||
let common_subkey_value_names = [r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"];
|
||||
for subkey_value in common_subkey_value_names {
|
||||
let path: Result<String, std::io::Error> = subkey.get_value(subkey_value);
|
||||
let Ok(path) = path else { continue };
|
||||
paths.insert(PathBuf::from(path).join("bin"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
/// Java entrypoint: Returns all possibly existing java paths on a macOS machine and validates them,
|
||||
/// returning a [`Vec<JavaVersion>`] with all valid java installations, their paths, and associated metadata.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tracing::instrument]
|
||||
pub async fn locate_java() -> Result<Vec<JavaVersion>, JavaError> {
|
||||
let mut paths = HashSet::new();
|
||||
paths.extend(locate_java_path().await);
|
||||
paths.extend(locate_installed_java().await?);
|
||||
|
||||
let jvm_base = PathBuf::from("/Library/Java/JavaVirtualMachines/");
|
||||
if let Ok(dir) = std::fs::read_dir(jvm_base) {
|
||||
for entry in dir.flatten() {
|
||||
let entry = entry.path().join("Contents/Home/bin");
|
||||
paths.insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
let common_java_paths = [
|
||||
r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands",
|
||||
r"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java",
|
||||
r"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home",
|
||||
];
|
||||
|
||||
for path in common_java_paths {
|
||||
paths.insert(PathBuf::from(path));
|
||||
}
|
||||
|
||||
let java = check_java(paths).await.into_iter().collect();
|
||||
|
||||
Ok(java)
|
||||
}
|
||||
|
||||
/// Java entrypoint: Returns all possibly existing java paths on a Linux machine and validates them,
|
||||
/// returning a [`Vec<JavaVersion>`] with all valid java installations, their paths, and associated metadata.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tracing::instrument]
|
||||
pub async fn locate_java() -> Result<Vec<JavaVersion>, JavaError> {
|
||||
let mut paths = HashSet::new();
|
||||
paths.extend(locate_java_path().await);
|
||||
paths.extend(locate_installed_java().await?);
|
||||
|
||||
let common_java_paths = [
|
||||
r"/usr",
|
||||
r"/usr/java",
|
||||
r"/usr/lib/jvm",
|
||||
r"/usr/lib64/jvm",
|
||||
r"/usr/lib32/jvm",
|
||||
r"/opt/jdk",
|
||||
r"/opt/jdks",
|
||||
r"/app/jdk",
|
||||
];
|
||||
|
||||
for path in common_java_paths {
|
||||
let path = PathBuf::from(path);
|
||||
paths.insert(PathBuf::from(&path).join("jre").join("bin"));
|
||||
paths.insert(PathBuf::from(&path).join("bin"));
|
||||
if let Ok(dir) = std::fs::read_dir(path) {
|
||||
for entry in dir.flatten() {
|
||||
let entry_path = entry.path();
|
||||
paths.insert(entry_path.join("jre").join("bin"));
|
||||
paths.insert(entry_path.join("bin"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let java = check_java(paths).await.into_iter().collect();
|
||||
|
||||
Ok(java)
|
||||
}
|
||||
|
||||
/// Locate Java paths automatically added by the launcher instance.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
async fn locate_installed_java() -> Result<HashSet<PathBuf>, JavaError> {
|
||||
Box::pin(async move {
|
||||
let mut paths = HashSet::new();
|
||||
let java_base_path = State::get()
|
||||
.await
|
||||
.map_err(|_| JavaError::MutexError)?
|
||||
.directories
|
||||
.java_dir()
|
||||
.await;
|
||||
|
||||
if java_base_path.is_dir() {
|
||||
if let Ok(dir) = std::fs::read_dir(java_base_path) {
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path().join("bin");
|
||||
if let Ok(contents) = io::read_to_string(path.clone()).await {
|
||||
let entry = entry.path().join(contents);
|
||||
paths.insert(entry);
|
||||
} else {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let path = path.join(JAVA_BIN);
|
||||
paths.insert(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Locates all installed Java instances from the `PATH` env variable, returning a
|
||||
/// [`HashSet<PathBuf>`] containing all paths in the `PATH` variable to be sorted into java versions.
|
||||
#[tracing::instrument]
|
||||
async fn locate_java_path() -> HashSet<PathBuf> {
|
||||
let paths = env::var("PATH").map(|p| env::split_paths(&p).collect::<HashSet<_>>());
|
||||
paths.unwrap_or_else(|_| HashSet::new())
|
||||
}
|
||||
|
||||
/// Verifies that each Java instance is valid from a [`HashSet<PathBuf>`], returning a
|
||||
/// [`HashSet<JavaVersion>`] for each validated path containing the installation's metadata.
|
||||
#[tracing::instrument]
|
||||
pub async fn check_java(paths: HashSet<PathBuf>) -> HashSet<JavaVersion> {
|
||||
let java_instances = stream::iter(paths.into_iter())
|
||||
.map(|p: PathBuf| tokio::task::spawn(async move { check_java_instance(&p).await }))
|
||||
.buffer_unordered(64)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
java_instances
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Verifies that a java instance [`Path`] is valid, returning a [`JavaVersion`] if the java path
|
||||
/// is valid, containing the installation's metadata.
|
||||
/// java_check from https://github.com/modrinth/theseus/blob/master/theseus/library/JavaInfo.class under MIT
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_macros::memory]
|
||||
pub async fn check_java_instance(path: &Path) -> Option<JavaVersion> {
|
||||
let Ok(path) = io::canonicalize(path) else {
|
||||
return None;
|
||||
};
|
||||
let java = if path.file_name()?.to_str()? == JAVA_BIN {
|
||||
path
|
||||
} else {
|
||||
path.join(JAVA_BIN)
|
||||
};
|
||||
if !java.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes = include_bytes!("../../wrapper/java/JavaInfo.class");
|
||||
let tempdir: PathBuf = tempfile::tempdir().ok()?.into_path();
|
||||
if !tempdir.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let file_path = tempdir.join("JavaInfo.class");
|
||||
tracing::info!(
|
||||
"checking java with {}",
|
||||
file_path.to_string_lossy().to_string()
|
||||
);
|
||||
io::write(&file_path, bytes).await.ok()?;
|
||||
|
||||
let output = Command::new(&java)
|
||||
.arg("-cp")
|
||||
.arg(file_path.parent().expect("failed to get java parent"))
|
||||
.arg("JavaInfo")
|
||||
.env_remove("_JAVA_OPTIONS")
|
||||
.output()
|
||||
.ok()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
tracing::debug!("{stdout}");
|
||||
|
||||
let mut java_version = None;
|
||||
let mut java_arch = None;
|
||||
|
||||
for line in stdout.lines() {
|
||||
let mut parts = line.split('=');
|
||||
let key = parts.next().unwrap_or_default();
|
||||
let value = parts.next().unwrap_or_default();
|
||||
|
||||
if key == "os.arch" {
|
||||
java_arch = Some(value);
|
||||
} else if key == "java.version" {
|
||||
java_version = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
arch: arch.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get a formatted java version as `(major: u32, minor: u32)`.
|
||||
///
|
||||
/// This means 1.8 becomes `(1, 8)`, and 21.0.3 becomes `(21, 0)`.
|
||||
pub fn get_java_version(version: &str) -> Result<(u32, u32), JavaError> {
|
||||
let mut split = version.split('.');
|
||||
let major_opt = split.next();
|
||||
let mut major;
|
||||
let mut minor = if let Some(minor) = split.next() {
|
||||
major = major_opt.unwrap_or("1").parse::<u32>()?;
|
||||
minor.parse::<u32>()?
|
||||
} else {
|
||||
major = 1;
|
||||
major_opt
|
||||
.ok_or_else(|| JavaError::InvalidVersion(version.to_string()))?
|
||||
.parse::<u32>()?
|
||||
};
|
||||
|
||||
if major > 1 {
|
||||
minor = major;
|
||||
major = 1;
|
||||
}
|
||||
|
||||
Ok((major, minor))
|
||||
}
|
||||
|
||||
/// An error wrapper for everything that can go wrong while managing Java installations.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum JavaError {
|
||||
#[error("failed to fetch environment variable: {0}")]
|
||||
EnvError(#[from] env::VarError),
|
||||
|
||||
#[error("error running java command: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("failed to parse integer: {0}")]
|
||||
ParseError(#[from] std::num::ParseIntError),
|
||||
|
||||
#[error("tokio error: {0}")]
|
||||
JoinError(#[from] JoinError),
|
||||
|
||||
#[error("couldn't get a valid java version from {0}")]
|
||||
InvalidVersion(String),
|
||||
|
||||
#[error("failed to get launcher state mutex!")]
|
||||
MutexError,
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
//! **`OneLauncher` Core Utilities**
|
||||
//! Standard asynchronous utilities and wrappers for use within the launcher core.
|
||||
//!
|
||||
//! - [`http`]: Async extensions and wrappers around [`reqwest`] functions.
|
||||
//! - [`java`]: Async utilities for managing and downloading Java versions.
|
||||
//! - [`watcher`]: Async utilities for watching files with [`notify`].
|
||||
|
||||
pub mod http;
|
||||
pub mod java;
|
||||
pub mod pagination;
|
||||
pub mod watcher;
|
||||
pub mod crypto;
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
pub mod window;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg_attr(feature = "specta", derive(specta::Type))]
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(dead_code)]
|
||||
pub struct Pagination {
|
||||
pub index: u32,
|
||||
pub page_size: u16,
|
||||
pub result_count: u32,
|
||||
pub total_count: u32,
|
||||
}
|
||||
|
||||
pub fn get_page_offset(page: Option<u32>, page_size: Option<u16>) -> (u32, u16) {
|
||||
let page = page.unwrap_or(0);
|
||||
let page_size = page_size.unwrap_or(10);
|
||||
let offset = (page - 1) * page_size as u32;
|
||||
(offset, page_size)
|
||||
}
|
||||
|
||||
pub fn get_page_count(total: u32, page_size: u16) -> u32 {
|
||||
(total as f64 / page_size as f64).ceil() as u32
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
//! **Watcher Utilities**
|
||||
//!
|
||||
//! Async utilities for watching files with [`notify`].
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::store::{Cluster, ClusterPath, Clusters};
|
||||
use futures::channel::mpsc::channel;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use notify::RecommendedWatcher;
|
||||
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
|
||||
|
||||
/// Creates and initializes an FS watcher for the `clusters` directory, returning
|
||||
/// the watcher as a [`Debouncer<RecommendedWatcher>`].
|
||||
pub fn initialize_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
let (mut sender, mut rscv) = channel(1);
|
||||
let watcher = new_debouncer(
|
||||
Duration::from_secs_f32(2.0),
|
||||
move |result: DebounceEventResult| {
|
||||
futures::executor::block_on(async {
|
||||
sender
|
||||
.send(result)
|
||||
.await
|
||||
.expect("failed to send debouse event");
|
||||
});
|
||||
},
|
||||
)?;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let span = tracing::span!(tracing::Level::INFO, "initialize_watcher");
|
||||
tracing::info!(parent: &span, "initializing fs watcher...");
|
||||
while let Some(result) = rscv.next().await {
|
||||
let _span = span.enter();
|
||||
|
||||
match result {
|
||||
Ok(mut events) => {
|
||||
let mut paths = Vec::new();
|
||||
events.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
for a in &events {
|
||||
let mut formatted = PathBuf::new();
|
||||
let mut components = a.path.components();
|
||||
let mut matched = false;
|
||||
|
||||
for cmp in components.by_ref() {
|
||||
formatted.push(cmp);
|
||||
if matched {
|
||||
break;
|
||||
}
|
||||
if cmp.as_os_str() == "clusters" {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
let sub = components.next().is_none();
|
||||
let cluster_path = ClusterPath::new(PathBuf::from(
|
||||
formatted.file_name().unwrap_or_default(),
|
||||
));
|
||||
|
||||
if a.path
|
||||
.components()
|
||||
.any(|c| c.as_os_str() == crate::constants::CRASH_PATH)
|
||||
&& a.path.extension().is_some_and(|e| e == "txt")
|
||||
{
|
||||
Cluster::handle_crash(cluster_path);
|
||||
} else if !paths.contains(&formatted) {
|
||||
if sub {
|
||||
// TODO: Packages Figure this out
|
||||
// let cluster_path = cluster_path.clone();
|
||||
// tokio::task::spawn(async move {
|
||||
// Cluster::sync_packages(&cluster_path, true).await
|
||||
// });
|
||||
paths.push(formatted);
|
||||
} else {
|
||||
Clusters::sync_cluster(cluster_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => tracing::warn!("fs watching error: {err}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
use tauri::WebviewWindow;
|
||||
|
||||
pub fn set_window_styling(
|
||||
win: &WebviewWindow,
|
||||
custom: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
win.set_decorations(!custom)?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if custom {
|
||||
win.set_title_bar_style(tauri::TitleBarStyle::Overlay)?;
|
||||
} else {
|
||||
win.set_title_bar_style(tauri::TitleBarStyle::Visible)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Library Wrappers
|
||||
|
||||
TODO: Don't pre-build binaries. That is a huge security issue.
|
||||
|
||||
## `JavaCheck`
|
||||
|
||||
A simple prebuilt Java class that prints the architecture and version of a JVM, [GitHub repository](https://github.com/PrismLauncher/PrismLauncher/tree/develop/libraries/javacheck) (Available under the Unlicense, sublicensed from ["Do what you want with it. It is so trivial that noone cares."](https://github.com/PrismLauncher/PrismLauncher/blob/develop/libraries/README.md#javacheck))
|
||||
|
||||
## `Gamemmode`
|
||||
|
||||
A performance optimization daemon, [GitHub repository](https://github.com/FeralInteractive/gamemode). (Available under `BSD-3-Clause` on their GitHub repository)
|
||||
|
||||
## `ForgeWrapper`
|
||||
|
||||
An interface for the launching systems of modern Minecraft Forge, [GitHub repository](https://github.com/PrismLauncher/ForgeWrapper). (Available under `MIT` on their GitHub repository)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
public final class JavaInfo {
|
||||
private static final String[] CHECKED_PROPERTIES = new String[] {"os.arch", "java.version", "java.vendor"};
|
||||
|
||||
public static void main(String[] args) {
|
||||
int returnCode = 0;
|
||||
|
||||
for (String key : CHECKED_PROPERTIES) {
|
||||
String property = System.getProperty(key);
|
||||
|
||||
if (property != null) {
|
||||
System.out.println(key + "=" + property);
|
||||
} else {
|
||||
returnCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
System.exit(returnCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ use std::{fmt::Display, ops::Deref, str::FromStr};
|
|||
use sea_orm::{sea_query::Nullable, DeriveValueType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
|
||||
// #[onelauncher_macro::specta]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType, specta::Type)]
|
||||
pub struct Icon(String);
|
||||
|
||||
impl Nullable for Icon {
|
||||
|
|
@ -73,7 +74,7 @@ pub enum IconType {
|
|||
|
||||
impl Display for IconType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use sea_orm::{sea_query::{Nullable, ValueType}, TryGetableFromJson};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Resolution {
|
||||
pub width: u32,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
cfg_match! {
|
||||
target_os = "windows" => {
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SettingsOsExtra {
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ cfg_match! {
|
|||
}
|
||||
}
|
||||
target_os = "macos" => {
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SettingsOsExtra {
|
||||
|
||||
|
|
@ -31,6 +33,7 @@ cfg_match! {
|
|||
}
|
||||
}
|
||||
not(any(target_os = "windows", target_os = "macos")) => {
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SettingsOsExtra {
|
||||
pub enable_gamemode: Option<bool>,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::ops::Deref;
|
|||
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct DbVec<T>(pub Vec<T>);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue