feat: Downloading entire game works now
This commit is contained in:
parent
81ed2de0f5
commit
2a0f53e107
14 changed files with 607 additions and 73 deletions
45
Cargo.lock
generated
45
Cargo.lock
generated
|
|
@ -2453,12 +2453,6 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
|
|
@ -2942,13 +2936,14 @@ dependencies = [
|
|||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"digest",
|
||||
"dirs",
|
||||
"discord-rich-presence",
|
||||
"dunce",
|
||||
"futures",
|
||||
"indicatif",
|
||||
"interpulse",
|
||||
"md5",
|
||||
"md-5",
|
||||
"murmur2",
|
||||
"onelauncher_entity",
|
||||
"onelauncher_gamemode",
|
||||
|
|
@ -2958,13 +2953,14 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"sanitize-filename",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_ini",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serde_with",
|
||||
"sha1_smol",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"specta",
|
||||
"sysinfo",
|
||||
|
|
@ -3730,14 +3726,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.6"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.7",
|
||||
"regex-syntax 0.8.4",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3751,13 +3747,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.4",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3768,9 +3764,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
|
|
@ -4007,6 +4003,15 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.21"
|
||||
|
|
@ -4404,12 +4409,6 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
|
|
|
|||
|
|
@ -117,12 +117,14 @@ async_zip = { version = "0.0.17", features = [ "full" ] }
|
|||
async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] }
|
||||
flate2 = { version = "1.0" }
|
||||
bytes = { version = "1.7" }
|
||||
sanitize-filename = { version = "0.6" }
|
||||
|
||||
# crypto
|
||||
sha1_smol = { version = "1.0", features = [ "std" ] }
|
||||
digest = { version = "0.10" }
|
||||
sha1 = { version = "0.10" }
|
||||
sha2 = { version = "0.10" }
|
||||
md-5 = { version = "0.10" }
|
||||
murmur2 = { version = "0.1.0" }
|
||||
md5 = { version = "0.7.0" }
|
||||
|
||||
# db
|
||||
sea-orm = { version = "1.1", features = [
|
||||
|
|
|
|||
|
|
@ -70,10 +70,11 @@ paste = { workspace = true }
|
|||
discord-rich-presence = { workspace = true }
|
||||
|
||||
# crypto
|
||||
sha1_smol = { workspace = true }
|
||||
digest = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
murmur2 = { workspace = true }
|
||||
md5 = { workspace = true }
|
||||
md-5 = { workspace = true }
|
||||
|
||||
# io
|
||||
zip = { workspace = true }
|
||||
|
|
@ -84,6 +85,7 @@ base64 = { workspace = true }
|
|||
async-compression = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
|
||||
# features[tauri]
|
||||
tauri = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ async fn main() -> LauncherResult<()> {
|
|||
let mut metadata = state.metadata.write().await;
|
||||
let versions = metadata.get_vanilla(GameLoader::Vanilla).await?;
|
||||
|
||||
let version = versions.versions.iter().find(|v| v.id == "1.8.9").ok_or_else(|| anyhow!("couldn't find 1.8.9 in version list"))?;
|
||||
let version = versions.versions.iter().find(|v| v.id == "1.20.4").ok_or_else(|| anyhow!("couldn't find 1.8.9 in version list"))?;
|
||||
let version_info = download_version_info(version, None, None, Some(true)).await?;
|
||||
|
||||
download_minecraft_ingressed(&version_info, Some(true)).await?;
|
||||
download_minecraft_ingressed(&version_info, "x86", Some(true)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use futures::{StreamExt, stream};
|
||||
use interpulse::api::minecraft::{AssetsIndex, Version, VersionInfo};
|
||||
use interpulse::api::minecraft::{AssetsIndex, DownloadType, Library, Os, OsRule, Rule, RuleAction, Version, VersionInfo};
|
||||
use interpulse::api::modded::LoaderVersion;
|
||||
use regex::Regex;
|
||||
use reqwest::Method;
|
||||
|
||||
use crate::api::ingress::{init_ingress, send_ingress_ref_opt};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::error::{LauncherError, LauncherResult};
|
||||
use crate::store::ingress::{IngressRef, IngressType};
|
||||
use crate::store::{Core, Dirs};
|
||||
use crate::utils::crypto::HashAlgorithm;
|
||||
use crate::utils::http::{self, fetch_json_advanced};
|
||||
use crate::utils::ext::OsExt;
|
||||
use crate::utils::http;
|
||||
use crate::utils::io;
|
||||
|
||||
pub async fn download_minecraft_ingressed(version: &VersionInfo, force: Option<bool>) -> LauncherResult<()> {
|
||||
pub async fn download_minecraft_ingressed(
|
||||
version: &VersionInfo,
|
||||
java_arch: &str,
|
||||
force: Option<bool>
|
||||
) -> LauncherResult<()> {
|
||||
let id = init_ingress(
|
||||
IngressType::MinecraftDownload,
|
||||
&format!("Downloading Minecraft {}", version.id),
|
||||
|
|
@ -21,20 +29,27 @@ pub async fn download_minecraft_ingressed(version: &VersionInfo, force: Option<b
|
|||
|
||||
let ingress_ref = IngressRef::new(&id, 100.0);
|
||||
|
||||
download_minecraft(version, Some(&ingress_ref), force).await
|
||||
download_minecraft(version, java_arch, Some(&ingress_ref), force).await
|
||||
}
|
||||
|
||||
// MARK: Main
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_minecraft(version: &VersionInfo, ingress_ref: Option<&IngressRef<'_>>, force: Option<bool>) -> LauncherResult<()> {
|
||||
pub async fn download_minecraft(
|
||||
version: &VersionInfo,
|
||||
java_arch: &str,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
force: Option<bool>
|
||||
) -> LauncherResult<()> {
|
||||
|
||||
const TASKS: f64 = 2.0;
|
||||
const TASKS: f64 = 4.0;
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / TASKS));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
|
||||
let asset_index = download_assets_index(version, ingress_ref, force).await?;
|
||||
tokio::try_join! {
|
||||
download_assets(version.assets == "legacy", &asset_index, ingress_ref, force)
|
||||
download_assets(version.assets == "legacy", &asset_index, ingress_ref, force),
|
||||
download_client(version, ingress_ref, force),
|
||||
download_libraries(&version.id, &version.libraries, java_arch, ingress_ref, force),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -48,6 +63,7 @@ pub async fn download_version_info(
|
|||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<VersionInfo> {
|
||||
tracing::debug!("loading version info for version {}", version.id);
|
||||
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 3.33));
|
||||
|
|
@ -70,11 +86,11 @@ pub async fn download_version_info(
|
|||
);
|
||||
|
||||
let mut info =
|
||||
fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress_ref).await?;
|
||||
http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress_ref).await?;
|
||||
|
||||
if let Some(loader) = loader {
|
||||
let partial: interpulse::api::modded::PartialVersionInfo =
|
||||
fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress_ref)
|
||||
http::fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress_ref)
|
||||
.await?;
|
||||
|
||||
info = interpulse::api::modded::merge_partial_version(partial, info);
|
||||
|
|
@ -100,6 +116,7 @@ pub async fn download_assets_index(
|
|||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<AssetsIndex> {
|
||||
tracing::debug!("loading assets index for version {}", version.id);
|
||||
let path = Dirs::get_assets_index_dir()
|
||||
.await?
|
||||
.join(version.asset_index.id.clone());
|
||||
|
|
@ -115,7 +132,7 @@ pub async fn download_assets_index(
|
|||
return Ok(data);
|
||||
}
|
||||
Err(err) => tracing::error!(
|
||||
"failed to read assets index from cache: {err:?}, downloading again",
|
||||
"failed to read assets index from cache: {err:?}, downloading...",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -135,13 +152,15 @@ pub async fn download_assets_index(
|
|||
}
|
||||
|
||||
// MARK: Assets
|
||||
/// Downloads the assets for a given asset index, returns the number of failed downloads
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_assets(
|
||||
legacy: bool,
|
||||
assets_index: &AssetsIndex,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<()> {
|
||||
) -> LauncherResult<usize> {
|
||||
tracing::debug!("loading asssets");
|
||||
let len = assets_index.objects.len();
|
||||
|
||||
let dir = if legacy {
|
||||
|
|
@ -176,13 +195,253 @@ pub async fn download_assets(
|
|||
)
|
||||
.await.map(|_| ())
|
||||
} else {
|
||||
// TODO: Possibly check hash? (not sure if this is a good idea here)
|
||||
Ok(())
|
||||
}
|
||||
}))
|
||||
.buffer_unordered(Core::get().fetch_attempts.min(7))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
requests.await;
|
||||
let mut failed = 0;
|
||||
for res in requests.await {
|
||||
if let Err(err) = res {
|
||||
tracing::error!("failed to download asset: {err:?}");
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
tracing::debug!("loaded assets");
|
||||
Ok(failed)
|
||||
}
|
||||
|
||||
// MARK: Client
|
||||
pub async fn download_client(
|
||||
version: &VersionInfo,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<PathBuf> {
|
||||
tracing::debug!("loading client for version {}", version.id);
|
||||
let client = version.downloads
|
||||
.get(&DownloadType::Client)
|
||||
.ok_or_else(|| anyhow::anyhow!("no client downloads exist for {}", &version.id))?;
|
||||
|
||||
let path = Dirs::get_versions_dir().await?.join(version.id.clone()).join(format!("{}.jar", version.id));
|
||||
if path.exists() && !force.unwrap_or(false) {
|
||||
let calculated_hash = HashAlgorithm::Sha1
|
||||
.hash_file(&path)
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("failed to hash file"))?;
|
||||
|
||||
if client.sha1 == calculated_hash {
|
||||
tracing::debug!("client already exists, skipping download");
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
tracing::warn!("client hash mismatch, redownloading");
|
||||
}
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
|
||||
http::download(Method::GET, &client.url, &path, Some((HashAlgorithm::Sha1, &client.sha1)), ingress_ref).await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
// MARK: Libraries
|
||||
/// Downloads the libraries and returns the number of failed downloads
|
||||
pub async fn download_libraries(
|
||||
version: &str,
|
||||
libraries: &[Library],
|
||||
java_arch: &str,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<usize> {
|
||||
tracing::debug!("loading libraries for version {}", version);
|
||||
|
||||
let lib_dir = Dirs::get_libraries_dir().await?;
|
||||
let natives_dir = Dirs::get_natives_dir().await?;
|
||||
|
||||
io::create_dir_all(&lib_dir).await?;
|
||||
io::create_dir_all(&natives_dir).await?;
|
||||
|
||||
let num_files = libraries.len();
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / num_files as f64));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
|
||||
let requests = stream::iter(libraries.iter().map(|lib| async {
|
||||
if let Some(rules) = &lib.rules {
|
||||
if !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);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?;
|
||||
let path = lib_dir.join(&artifact_path);
|
||||
|
||||
if path.exists() && !force.unwrap_or(false) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(interpulse::api::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact), ..
|
||||
}) = lib.downloads {
|
||||
if !artifact.url.is_empty() {
|
||||
http::download(
|
||||
Method::GET,
|
||||
&artifact.url,
|
||||
&path,
|
||||
Some((HashAlgorithm::Sha1, &artifact.sha1)),
|
||||
ingress_ref,
|
||||
).await?;
|
||||
|
||||
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(
|
||||
Method::GET,
|
||||
&url,
|
||||
&path,
|
||||
None,
|
||||
ingress_ref,
|
||||
).await?;
|
||||
|
||||
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
|
||||
Ok::<_, LauncherError>(())
|
||||
},
|
||||
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) {
|
||||
tracing::trace!("found native library {}", &lib.name);
|
||||
let data = http::fetch_advanced(
|
||||
Method::GET,
|
||||
&native.url,
|
||||
None,
|
||||
Some((HashAlgorithm::Sha1, &native.sha1)),
|
||||
None,
|
||||
ingress_ref,
|
||||
).await?;
|
||||
|
||||
let dest = natives_dir.join(version);
|
||||
io::unzip_bytes(data.to_vec(), &dest).await?;
|
||||
tracing::trace!("extracted native {} to path {:?}", &lib.name, &dest);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
})).buffer_unordered(7).collect::<Vec<_>>();
|
||||
|
||||
let mut failed = 0;
|
||||
for res in requests.await {
|
||||
if let Err(err) = res {
|
||||
tracing::error!("failed to download asset: {err:?}");
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(failed)
|
||||
}
|
||||
|
||||
// MARK: Rules
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn validate_rules(rules: &[Rule], java_version: &str, updated: bool) -> bool {
|
||||
let mut rule = rules
|
||||
.iter()
|
||||
.map(|r| validate_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(level = "debug")]
|
||||
fn validate_rule(rule: &Rule, java_version: &str, updated: bool) -> Option<bool> {
|
||||
let result = match rule {
|
||||
Rule {
|
||||
os: Some(os), ..
|
||||
} => validate_os_rule(os, java_version, updated),
|
||||
Rule {
|
||||
features: Some(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
|
||||
let mut rule_match = true;
|
||||
|
||||
if let Some(ref arch) = rule.arch {
|
||||
rule_match &= !matches!(arch.as_str(), "x86" | "arm");
|
||||
}
|
||||
|
||||
if let Some(name) = &rule.name {
|
||||
if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) {
|
||||
rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name;
|
||||
} else {
|
||||
rule_match &= &Os::native_arch(java_arch) == name;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &rule.version {
|
||||
if let Ok(regex) = Regex::new(version.as_str()) {
|
||||
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
rule_match
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ pub enum LauncherError {
|
|||
DbError(#[from] sea_orm::DbErr),
|
||||
#[error(transparent)]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
#[error(transparent)]
|
||||
InterpulseError(#[from] interpulse::Error),
|
||||
#[error("couldn't acquire semaphore: {0}")]
|
||||
SemaphoreError(#[from] tokio::sync::AcquireError),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#![feature(let_chains)]
|
||||
#![feature(slice_as_array)]
|
||||
|
||||
use api::proxy::LauncherProxy;
|
||||
use store::{proxy::ProxyState, semaphore::SemaphoreStore, Core, CoreOptions, Dirs, State};
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ pub struct IngressRef<'a> {
|
|||
pub increment_by: f64,
|
||||
}
|
||||
|
||||
unsafe impl Send for IngressRef<'_> {}
|
||||
unsafe impl Sync for IngressRef<'_> {}
|
||||
|
||||
impl<'a> IngressRef<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(ingress_id: &'a IngressId, increment_by: f64) -> Self {
|
||||
|
|
|
|||
|
|
@ -177,21 +177,19 @@ impl Metadata {
|
|||
($(($var:tt, $is_modded:tt)),*) => {
|
||||
let ($($var),*) = tokio::join!(
|
||||
$(
|
||||
tokio::spawn(async move {
|
||||
async move {
|
||||
if let Ok(loader) = GameLoader::from_str(stringify!(minecraft)) {
|
||||
fetch_manifest!(loader, $is_modded).map_err(|e| $crate::send_error!("couldn't fetch {loader} manifest: {e}")).ok()
|
||||
} else {
|
||||
tracing::error!("failed to fetch {} manifest", stringify!($var));
|
||||
None
|
||||
}
|
||||
}),
|
||||
},
|
||||
)*
|
||||
);
|
||||
|
||||
$(
|
||||
if let Ok(data) = $var {
|
||||
self.inner.$var = data;
|
||||
}
|
||||
self.inner.$var = $var;
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
use std::{fmt::Display, str::FromStr};
|
||||
use std::fmt::{Display, Write};
|
||||
use std::io::Read;
|
||||
use std::str::FromStr;
|
||||
|
||||
use digest::DynDigest;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::io::IOError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CryptoError {
|
||||
|
|
@ -9,20 +16,89 @@ pub enum CryptoError {
|
|||
actual: String,
|
||||
},
|
||||
#[error("invalid hash algorithm")]
|
||||
InvalidAlgorithm
|
||||
InvalidAlgorithm,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HashAlgorithm {
|
||||
Sha1,
|
||||
Sha256,
|
||||
Md5,
|
||||
}
|
||||
|
||||
const BUFFER_SIZE: usize = 1024 * 256; // 16 KiB
|
||||
|
||||
impl HashAlgorithm {
|
||||
#[must_use]
|
||||
pub fn hash(&self, data: &[u8]) -> String {
|
||||
match self {
|
||||
Self::Sha1 => sha1_smol::Sha1::from(data).digest().to_string(),
|
||||
}
|
||||
pub fn hasher_sync<F>(&self, closure: F) -> Result<String, IOError>
|
||||
where
|
||||
F: Fn(&mut (dyn DynDigest)) -> Result<(), IOError>,
|
||||
{
|
||||
let mut hasher = get_inner(self);
|
||||
closure(&mut *hasher)?;
|
||||
Ok(to_hex(&hasher.finalize()))
|
||||
}
|
||||
|
||||
pub async fn hasher<F>(&self, closure: F) -> Result<String, IOError>
|
||||
where
|
||||
F: AsyncFn(&mut (dyn DynDigest)) -> Result<(), IOError>,
|
||||
{
|
||||
let mut hasher = get_inner(self);
|
||||
closure(&mut *hasher).await?;
|
||||
Ok(to_hex(&hasher.finalize()))
|
||||
}
|
||||
|
||||
pub async fn hash(&self, data: &[u8]) -> Result<String, IOError> {
|
||||
self.hasher(async move |hasher| {
|
||||
hasher.update(data);
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
pub fn hash_sync(&self, data: &[u8]) -> Result<String, IOError> {
|
||||
self.hasher_sync(move |hasher| {
|
||||
hasher.update(data);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn hash_file(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
|
||||
self.hasher(async |hasher: &mut dyn DynDigest| {
|
||||
let path = path.as_ref();
|
||||
let file = tokio::fs::File::open(path).await?;
|
||||
let mut reader = tokio::io::BufReader::new(file);
|
||||
|
||||
let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice();
|
||||
loop {
|
||||
let bytes_read = reader.read(&mut buffer).await?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
pub fn hash_file_sync(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
|
||||
self.hasher_sync(|hasher: &mut dyn DynDigest| {
|
||||
let path = path.as_ref();
|
||||
let file = std::fs::File::open(path)?;
|
||||
let mut reader = std::io::BufReader::new(file);
|
||||
|
||||
let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice();
|
||||
loop {
|
||||
let bytes_read = reader.read(&mut buffer)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -30,6 +106,8 @@ impl Display for HashAlgorithm {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Sha1 => write!(f, "sha1"),
|
||||
Self::Sha256 => write!(f, "sha256"),
|
||||
Self::Md5 => write!(f, "md5"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,8 +117,25 @@ impl FromStr for HashAlgorithm {
|
|||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"sha1" => Ok(Self::Sha1),
|
||||
"sha1" | "sha-1" => Ok(Self::Sha1),
|
||||
"sha256" | "sha-256" => Ok(Self::Sha256),
|
||||
"md5" | "md-5" => Ok(Self::Md5),
|
||||
_ => Err(CryptoError::InvalidAlgorithm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_inner(algorithm: &HashAlgorithm) -> Box<dyn DynDigest> {
|
||||
match algorithm {
|
||||
HashAlgorithm::Sha1 => Box::new(sha1::Sha1::default()),
|
||||
HashAlgorithm::Sha256 => Box::new(sha2::Sha256::default()),
|
||||
HashAlgorithm::Md5 => Box::new(md5::Md5::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn to_hex(data: &[u8]) -> String {
|
||||
data.iter().fold(String::new(), |mut output, b| {
|
||||
let _ = write!(output, "{b:02x}");
|
||||
output
|
||||
})
|
||||
}
|
||||
48
packages/core/src/utils/ext.rs
Normal file
48
packages/core/src/utils/ext.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use interpulse::api::minecraft::Os;
|
||||
|
||||
/// An extension of [`Os`] for utilities on converting the current system's operating system
|
||||
/// into an [`Os`] structure for simple management.
|
||||
pub trait OsExt {
|
||||
/// Get the [`Os`] of the current system
|
||||
fn native() -> Self;
|
||||
|
||||
/// Get the [`Os`] of the current system along with it's arch from the `java_arch`.
|
||||
fn native_arch(java_arch: &str) -> Self;
|
||||
}
|
||||
|
||||
impl OsExt for Os {
|
||||
fn native_arch(java_arch: &str) -> Self {
|
||||
if std::env::consts::OS == "windows" {
|
||||
if java_arch == "aarch64" {
|
||||
Self::WindowsArm64
|
||||
} else {
|
||||
Self::Windows
|
||||
}
|
||||
} else if std::env::consts::OS == "linux" {
|
||||
if java_arch == "aarch64" {
|
||||
Self::LinuxArm64
|
||||
} else if java_arch == "arm" {
|
||||
Self::LinuxArm32
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
} else if std::env::consts::OS == "macos" {
|
||||
if java_arch == "aarch64" {
|
||||
Self::OsxArm64
|
||||
} else {
|
||||
Self::Osx
|
||||
}
|
||||
} else {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn native() -> Self {
|
||||
match std::env::consts::OS {
|
||||
"windows" => Self::Windows,
|
||||
"macos" => Self::Osx,
|
||||
"linux" => Self::Linux,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -123,15 +123,21 @@ pub async fn fetch_advanced(
|
|||
|
||||
if let Some((ref algorithm, expected_hash)) = hash {
|
||||
let expected_hash = expected_hash.to_string();
|
||||
let calculated_hash = algorithm.hash(&bytes);
|
||||
|
||||
if *calculated_hash != expected_hash {
|
||||
return Err(CryptoError::InvalidHash {
|
||||
algorithm: algorithm.clone(),
|
||||
expected: expected_hash,
|
||||
actual: calculated_hash,
|
||||
match algorithm.hash(&bytes).await {
|
||||
Ok(calculated_hash) => {
|
||||
if *calculated_hash != expected_hash {
|
||||
return Err(CryptoError::InvalidHash {
|
||||
algorithm: algorithm.clone(),
|
||||
expected: expected_hash,
|
||||
actual: calculated_hash,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("failed to calculate hash for {url}: {err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use async_compression::tokio::bufread::GzipDecoder;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IOError {
|
||||
#[error("invalid absolute path '{0}'")]
|
||||
InvalidAbsolutePath(std::path::PathBuf),
|
||||
InvalidAbsolutePath(PathBuf),
|
||||
|
||||
#[error("error acessing path: {source}, path: {path}")]
|
||||
IOErrorWrapper {
|
||||
|
|
@ -21,6 +24,8 @@ pub enum IOError {
|
|||
IOError(#[from] std::io::Error),
|
||||
#[error("json deserialization error: {0}")]
|
||||
DeserializeError(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
AsyncZipError(#[from] async_zip::error::ZipError),
|
||||
}
|
||||
|
||||
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
|
||||
|
|
@ -37,12 +42,12 @@ impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
|
|||
pub struct NonUtf8PathError(pub Box<std::path::Path>);
|
||||
|
||||
/// Attempts to parse a path taken from an environment variable. Returns `None` if the variable is not set.
|
||||
pub fn env_path(name: &str) -> Option<std::path::PathBuf> {
|
||||
std::env::var_os(name).map(std::path::PathBuf::from)
|
||||
pub fn env_path(name: &str) -> Option<PathBuf> {
|
||||
std::env::var_os(name).map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// An OS specific wrapper of [`std::fs::canonicalize`], but on Windows it outputs the most compatible form of a path instead of UNC.
|
||||
pub fn canonicalize(path: impl AsRef<std::path::Path>) -> Result<std::path::PathBuf, IOError> {
|
||||
pub fn canonicalize(path: impl AsRef<std::path::Path>) -> Result<PathBuf, IOError> {
|
||||
let path = path.as_ref();
|
||||
dunce::canonicalize(path).map_err(|e| IOError::IOErrorWrapper {
|
||||
source: e,
|
||||
|
|
@ -134,11 +139,13 @@ pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError>
|
|||
}
|
||||
|
||||
/// Asynchronously read a file as JSON and return the deserialized object
|
||||
pub async fn read_json<T: DeserializeOwned>(path: impl AsRef<std::path::Path>) -> Result<T, IOError> {
|
||||
pub async fn read_json<T: DeserializeOwned>(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<T, IOError> {
|
||||
Ok(serde_json::from_slice(&read(path).await?)?)
|
||||
}
|
||||
|
||||
/// Asynchrously write to a tempfile that is then transferred to an official [`AsRef<Path>`].
|
||||
/// Asynchrously write to a file.
|
||||
pub async fn write(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
data: impl AsRef<[u8]>,
|
||||
|
|
@ -152,10 +159,26 @@ pub async fn write(
|
|||
})
|
||||
}
|
||||
|
||||
/// Asynchronously write buffered data to a file, creating it if it does not exist.
|
||||
pub async fn write_buf<F>(path: impl AsRef<std::path::Path>, f: F) -> Result<(), IOError>
|
||||
where
|
||||
F: AsyncFnOnce(&mut tokio::io::BufWriter<tokio::fs::File>) -> Result<(), IOError>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let file = tokio::fs::File::create(path).await?;
|
||||
let mut writer = tokio::io::BufWriter::new(file);
|
||||
|
||||
f(&mut writer).await?;
|
||||
|
||||
writer.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Asynchronously write json to a file, creating it if it does not exist.
|
||||
pub async fn write_json<T: Serialize>(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
data: T
|
||||
data: T,
|
||||
) -> Result<(), IOError> {
|
||||
write(path, serde_json::to_vec(&data)?).await
|
||||
}
|
||||
|
|
@ -210,3 +233,98 @@ pub fn tempdir() -> Result<TempDir, IOError> {
|
|||
pub fn tempfile() -> Result<std::fs::File, IOError> {
|
||||
Ok(tempfile::tempfile()?)
|
||||
}
|
||||
|
||||
/// Unzips a zip archive from a byte array
|
||||
pub async fn unzip_bytes(
|
||||
data: Vec<u8>,
|
||||
dest_path: impl AsRef<std::path::Path>
|
||||
) -> Result<(), IOError> {
|
||||
let reader = async_zip::base::read::mem::ZipFileReader::new(data).await?;
|
||||
|
||||
let entries = reader.file().entries();
|
||||
for index in 0..entries.len() {
|
||||
let entry = entries.get(index).expect("expected more zip entries");
|
||||
let path = dest_path.as_ref().join(sanitize_path(entry.filename().as_str()?));
|
||||
|
||||
let is_dir = entry.dir()?;
|
||||
|
||||
let mut entry_reader = reader
|
||||
.reader_without_entry(index)
|
||||
.await
|
||||
.expect("Failed to read ZipEntry");
|
||||
|
||||
if is_dir {
|
||||
create_dir_all(path).await?;
|
||||
} else {
|
||||
if let Some(parent) = path.parent() {
|
||||
create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let file = tokio::fs::File::create(&path).await?;
|
||||
let mut writer = tokio::io::BufWriter::new(file);
|
||||
let mut buf = vec![0; 8192];
|
||||
loop {
|
||||
let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&buf[..n]).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unzips a zip archive from a file
|
||||
pub async fn unzip_file(
|
||||
zip_path: impl AsRef<std::path::Path>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let zip_path = zip_path.as_ref();
|
||||
let dest_path = dest_path.as_ref();
|
||||
|
||||
let reader = async_zip::tokio::read::fs::ZipFileReader::new(zip_path).await?;
|
||||
let entries = reader.file().entries();
|
||||
|
||||
for index in 0..entries.len() {
|
||||
let entry = entries
|
||||
.get(index)
|
||||
.expect("expected more zip entries");
|
||||
|
||||
let path = dest_path.join(sanitize_path(entry.filename().as_str()?));
|
||||
let is_dir = entry.dir()?;
|
||||
|
||||
if is_dir {
|
||||
create_dir_all(&path).await?;
|
||||
} else {
|
||||
if let Some(parent) = path.parent() {
|
||||
create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let file = tokio::fs::File::create(&path).await?;
|
||||
let mut writer = tokio::io::BufWriter::new(file);
|
||||
let mut entry_reader = reader.reader_without_entry(index).await?;
|
||||
|
||||
let mut buf = vec![0; 8192];
|
||||
loop {
|
||||
let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&buf[..n]).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sanitize_path(path: impl AsRef<std::path::Path>) -> PathBuf {
|
||||
path.as_ref()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
.split('/')
|
||||
.map(sanitize_filename::sanitize)
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod crypto;
|
||||
pub mod ext;
|
||||
pub mod http;
|
||||
pub mod io;
|
||||
Loading…
Add table
Reference in a new issue